AWS Smart Territory FrameworkでFIWAREを試してみた（その1：ブローカー）

2022年の9/14〜15に行われた[FIWARE Global Summit](https://www.fiware.org/global-summit/)でも[触れられていました](https://mobile.twitter.com/HSundmaeker/status/1569975037762965505)が、[AWS](https://aws.amazon.com/)がFIWARE Foundationにjoinし、smartな都市の開発で協力していくとの方針が2022/7/12に[発表](https://www.fiware.org/news/fiware-foundation-and-aws-join-forces-to-develop-digital-cities/)されています。

その一環として、AWSから[aws-stf](https://github.com/aws-samples/aws-stf)が公開されました。このSTF（Smart Territory Framework）はAWS上にFIWAREのエコシステムを構築するためのツールキットであり、[AWS CDK](https://aws.amazon.com/jp/cdk/)を用いることで複雑なインフラをTypeScript及びPythonなどで宣言的に定義し、容易に環境を立ち上げることができるようになっています。

そこで今回は、aws-stfを用いてAWS上にFIWAREエコシステムを立ち上げ、実際にどのような環境が構築されるのか確認してみたいと思います。

![STF core scorpio](https://github.com/aws-samples/aws-stf-core-scorpio/blob/main/docs/images/scorpio_arch.png?raw=true)
> 出典： [Smart Territory Framework Core - STF Core using NEC Scorpio Broker](https://github.com/aws-samples/aws-stf-core-scorpio)

# 検証環境
今回は以下の環境で検証を行いました。aws cliとnode.jsはイイカンジにインストールしておいてください。AWSの認証情報もさくっと設定しておいておいてください（私は面倒だったので、AdministoratorAccessを持つアカウントを使いました）。

|ツール|バージョン|
|:--|:--|
|AWS CLI|aws-cli/2.7.31 Python/3.10.6 Darwin/20.6.0 source/x86_64 prompt/off|
|node.js|v16.17.0|

```ini:~/.aws/config
[default]
output = json
region = us-west-2
```
```ini:~/.aws/credentials
[default]
aws_secret_access_key = xxxxxxxx
aws_access_key_id = xxxxxxxx
```

## 注意
この記事を書いている2022/09/18時点では、regionは **us-west-2（オレゴン）** を選択するのが無難です。今回利用するScorpio版のSTFは内部的に[AWS Aurora Serverless](https://aws.amazon.com/jp/rds/aurora/serverless/)や[Amazon Managed Streaming for Apache Kafka](https://aws.amazon.com/jp/msk/)を立ち上げますが、 バージニア北部やオハイオ、北カリフォルニア、東京といったregionでは、以下のようなエラーが発生して構築途中で失敗し、ROLLBACKすることがあります。これはそれらのregionのAvailability Zoneには、Aurora ServerlessやMSKに対応していないものがあるためです[^1]。
CDKのnetworkまわりの[コード](https://github.com/aws-samples/aws-stf-core-scorpio/blob/main/lib/stacks/scorpio-serverless/networking/index.ts)に手を入れて、regionに対応する適切なAZを指定できるようにすると良いかもしれませんが、とりあえず今回はコードには手を入れずにus-west-2で検証をすすめることにします。

> Your subnet group only has one Availability Zone us-west-1a that is supported by Aurora Serverless. 

> One or more subnets belong to unsupported availability zones: [us-east-1b]. (Service: Kafka, Status Code: 400

[^1]: https://docs.aws.amazon.com/msk/latest/developerguide/msk-create-cluster.html


# STFスタックの起動
それではさくっとSTFスタックを起動します。
FIWAREエコシステムの根幹となるブローカー機能には、よく使われているNGSI v1/v2対応の[FIWARE orion](https://github.com/telefonicaid/fiware-orion)やそのNGSI-LD対応版である[orion-ld](https://github.com/FIWARE/context.Orion-LD)、NGSI-LD用に書き起こされた[Scorpio](https://github.com/ScorpioBroker/ScorpioBroker)や[stellio](https://github.com/stellio-hub/stellio-context-broker/)など複数のコンポーネントがあります。STFは構想上、複数のブローカーコンポーネントに対応するつもりのようですが、9/18時点ではScorpioとAWS Serverless Serviceを組み合わせたリリースが利用可能となっています。

> We provide below a list of the different flavors of the STF Core integrating the multiple implementations of the FIWARE Context Broker available.

## aws-stf（scorpio版）の取得
```bash
nmatsui@:aws-stf$ git clone https://github.com/aws-samples/aws-stf-core-scorpio.git
nmatsui@:aws-stf$ cd aws-stf-core-scorpio/
nmatsui@:aws-stf-core-scorpio (main =)$ 
```
## CDKのライブラリインストール
9/18時点でnode.js用のCDKは2.42.0が最新ですが、リポジトリで指定されている2.13.0をローカルインストールして利用することにします。
```bash
nmatsui@:aws-stf-core-scorpio (main =)$ npm install
nmatsui@:aws-stf-core-scorpio (main =)$ npx cdk --version
2.13.0 (build b0b744d)
```
## CDKの準備
まずはbootstrapを実行して、CDKが必要とするアセットを指定したregion上に準備します。二回目以降はbootstrapを実行する必要はありません。
```bash
nmatsui@:aws-stf-core-scorpio (main =)$ npx cdk bootstrap
 ⏳  Bootstrapping environment aws://xxxxxxxxxxxx/us-west-2...
Trusted accounts for deployment: (none)
Trusted accounts for lookup: (none)
Using default execution policy of 'arn:aws:iam::aws:policy/AdministratorAccess'. Pass '--cloudformation-execution-policies' to customize.
CDKToolkit: creating CloudFormation changeset...

 ✅  Environment aws://xxxxxxxxxxxx/us-west-2 bootstrapped.
```
## STFスタックのデプロイ
では、本命であるSTFスタックをデプロイします。30~45分程度かかりますので、気長に待ちましょう。
```bash
nmatsui@:aws-stf-core-scorpio (main =)$ npx cdk deploy

✨  Synthesis time: 6.4s

StfCore: deploying...
[0%] start: Publishing 4a575666d1c2c6412590d2a56f328e040a81ad1ef59aecee31ae9b393d05f659:current_account-current_region

 ✅  StfCore

✨  Deployment time: 2264.27s

Outputs:
StfCore.StfCoreEndpoint = https://xxxxxxxx.execute-api.us-west-2.amazonaws.com
StfCore.StfCoreIotQueueArn = arn:aws:sqs:us-west-2:xxxxxxxxxxxx:StfIoTQueue-us-west-2
Stack ARN:
arn:aws:cloudformation:us-west-2:xxxxxxxxxxxx:stack/StfCore/490d76b0-36d0-11ed-bd00-029c3f5cbc51

✨  Total time: 2270.67s
```

# CDKが起動した環境の確認
それでは、CDKによって構築された環境を確認していきましょう。実はこの段階で、ブローカー機能だけでなくIoTデバイスとブローカーを接続するスタック（STF IoT）も立ち上がっています。ただし今回は、ブローカー機能に必要な環境だけ確認することにします。

## CloudFormationスタック
AWS CDKは内部的に[AWS CloudFormation](https://aws.amazon.com/jp/cloudformation/)のテンプレートを生成してAWS上のリソースを立ち上げています。STFスタックは、実際は**ScorpioServerlessNestedStack**と**IoTNestedStack**から構成されている事がわかります。
```bash
nmatsui@:aws-stf-core-scorpio (main =)$ aws cloudformation list-stacks --stack-status-filter CREATE_COMPLETE --query "StackSummaries[].StackName"
[
    "StfCore-IoTNestedStackIoTNestedStackResource15FB5E08-19AKRXNQCBVG",
    "StfCore-ScorpioServerlessNestedStackScorpioServerlessNestedStackResourceF6005A66-1ASHOUNTIXFG0",
    "StfCore",
    "CDKToolkit"
]
```
## AWS Aurora Serverless
今回利用するScorpioは、内部的にApache KafkaとPostgreSQLに依存しています[^2]。STFはこのPostgreSQLとして、 [AWS Aurora Serverless](https://aws.amazon.com/jp/rds/aurora/serverless/)をPostreSQLエンジンで起動して利用しています。Aurora Serverlessを利用することで、負荷が少ない場合はコストが安くすむようになっています。

[^2]: https://github.com/ScorpioBroker/ScorpioBroker/blob/development/docs/en/source/installationGuide.rst

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ aws rds describe-db-clusters --query "DBClusters[].{name:DatabaseName,engine:Engine,version:EngineVersion,mode:EngineMode}"
[
    {
        "name": "scorpio",
        "engine": "aurora-postgresql",
        "version": "10.18",
        "mode": "serverless"
    }
]
```
## Amazon Managed Streaming for Apache Kafka
Scorpioが依存するkafkaのために、Apache Kafkaのマネージドサービスが起動されています。おそらくコストの問題で、最小ノードサイズの2brokerしか起動させていないのだと思われます。（コストが問題なのであれば、MSK Serverlessにする手もあるように思いますが、Serverlessにできない理由が何かあるのでしょうかね？）

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ aws kafka list-clusters --query "ClusterInfoList[].{name:ClusterName,kafkaVersion:CurrentBrokerSoftwareInfo.KafkaVersion,numberOfBrokerNodes:NumberOfBrokerNodes,size:BrokerNodeGroupInfo.InstanceType}"
[
    {
        "name": "ScorpioCluster",
        "kafkaVersion": "2.8.1",
        "numberOfBrokerNodes": 2,
        "size": "kafka.t3.small"
    }
]
```
## Scorpio本体
Scorpio本体は、Fargate上のコンテナとして起動しています。active-activeで２タスクを上げているようです。[Scorpioの公式docker image](https://hub.docker.com/r/scorpiobroker/scorpio/)ではなく、[Amazon ECR](https://aws.amazon.com/jp/ecr/)上のカスタムイメージ[scorpiobroker/scorpio-aio](https://gallery.ecr.aws/scorpiobroker/scorpio-aio)を使っています。このカスタムイメージと公式イメージに差分があるのかは、確認していません。

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ export ECS_CLUSTER=$(aws ecs list-clusters --query "clusterArns[0]" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ export ECS_SERVICE=$(aws ecs list-services --cluster $ECS_CLUSTER --query "serviceArns[0]" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ export ECS_TASK=$(aws ecs list-tasks --cluster $ECS_CLUSTER --service-name $ECS_SERVICE --query "taskArns[0]" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ aws ecs describe-services --cluster $ECS_CLUSTER --services $ECS_SERVICE --query "services[].{launchType:launchType,desiredCount:desiredCount,runningCount:runningCount}"
[
    {
        "launchType": "FARGATE",
        "desiredCount": 2,
        "runningCount": 2
    }
]
nmatsui@:aws-stf-core-scorpio (main =)$ aws ecs describe-tasks --cluster $ECS_CLUSTER --tasks $ECS_TASK --query "tasks[].{containers:containers[].{image:image,status:lastStatus},cpu:cpu,memory:memory}"
[
    {
        "containers": [
            {
                "image": "public.ecr.aws/scorpiobroker/scorpio-aio:latest",
                "status": "RUNNING"
            }
        ],
        "cpu": "512",
        "memory": "2048"
    }
]
```
なお、Scorpioのイメージは`parameters.ts`で指定されているようです。公式イメージにしても、Aurora ServerlessやMSKで動作するのかな？
```typescript:parameters.ts
export const Parameters = {
    image_context_broker: 'public.ecr.aws/scorpiobroker/scorpio-aio:latest',
    smart_data_model_url : 'https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld',
    shadow_prefix: "Stf",
    timeout: '0', // Timeout for the API call in the Lambda that sync with context broker. Has to be a string to pass it in env variable
}
```
## Amazon API Gateway
最後は[Amazon API Gateway](https://aws.amazon.com/jp/api-gateway/)です。インターネット上にエンドポイントを公開し、Scoprioのコンテナへリクエストをルーティングします。
なお `/iot/things/*` への `GET,POST,DELETE` は、今回説明しないSTF IoTスタックが作成したIoTデバイス用のルートです。それ以外のリクエストは `ANY /{proxy+}` ルートにヒットして、VPCLink経由でScorpioのコンテナへルーティングされます。

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ export API_ID=$(aws apigatewayv2 get-apis --query "Items[0].ApiId" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ aws apigatewayv2 get-routes --api-id $API_ID --query "Items[].RouteKey"
[
    "GET /iot/things",
    "GET /iot/things/{thingName}",
    "DELETE /iot/things/{thingName}",
    "ANY /{proxy+}",
    "POST /iot/things"
]
```
では、API Gatewayのエンドポイントに対して `/actuator/info` や `/actuator/health` というパスでGETリクエストを投げてみます。Scorpioがそれらのリクエストを受け付けると、内部的に利用されているSping bootがビルド情報や動作状況を返します。

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ export ENDPOINT=$(aws apigatewayv2 get-apis --query "Items[0].ApiEndpoint" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sS "$ENDPOINT/actuator/info" | jq .
{
  "build": {
    "artifact": "AllInOneRunner",
    "name": "AllInOneRunner",
    "time": "2022-09-08T13:51:46.913Z",
    "version": "1.1.0-SNAPSHOT",
    "group": "eu.neclab.ngsildbroker"
  }
}
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sS "$ENDPOINT/actuator/health" | jq .
{
  "status": "UP"
}
```

# Amazon API Gatewayへトークン認証を追加
さて、無事にScorpioが動作してインターネットから接続できることが確認できたのですが、ブローカーがインターネット上へ生で公開されているのは、検証用とはいえさすがに問題です。[STF Core using NEC Scorpio BrokerスタックのREADME](https://github.com/aws-samples/aws-stf-core-scorpio/blob/main/README.md)でも、**プロダクションで利用する前にアクセスコントロールを考えなさい**と注意されています。

> **Before using this stack in production, please consider adding a mechanism for controlling and managing access to the StfCoreEndpoint.**

本来は、Entityの`GET`は無制限だが`POST/PATCH/DELETE`は個別にリクエストを認証して認可された利用者のみ許可、のような認証認可戦略を立て実現すべきですが、今回は検証用にすべてのルートに対して簡易的なトークン認証を実装します。

## 認証用lambda functionの登録
リクエストの"authorization"ヘッダが特定の文字列であることをチェックする、非常にシンプルな認証用lambda functionを登録します。
<details><summary>認証用lambda functionの登録</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ export TOKEN=your_token
nmatsui@:aws-stf-core-scorpio (main =)$ export REGION=$(aws configure get region --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ export ACCOUNT=$(aws sts get-caller-identity --query "Account" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ aws iam create-role --role-name lambda-auth-role \
--assume-role-policy-document '{"Version": "2012-10-17","Statement": [{ "Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'
nmatsui@:aws-stf-core-scorpio (main =)$ cat <<__EOF__ > lambda-auth.js
exports.handler = async(event) => {
    let response = {
        "isAuthorized": false
    };
    
    if (event.headers.authorization === "$TOKEN") {
        response = {
            "isAuthorized": true
        };
    }
    return response;
};
__EOF__
nmatsui@:aws-stf-core-scorpio (main =)$ zip function.zip lambda-auth.js
nmatsui@:aws-stf-core-scorpio (main =)$ aws lambda create-function --function-name lambda-auth \
--zip-file fileb://function.zip \
--handler lambda-auth.handler \
--runtime nodejs12.x \
--role arn:aws:iam::$ACCOUNT:role/lambda-auth-role
```
</div></details>

## Authorizerの作成
作成済みのlambda funtionを、Amazon API Gatewayのauthrizerとして登録します。
<details><summary>Authorizerの作成</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ aws apigatewayv2 create-authorizer \
    --api-id $API_ID \
    --authorizer-type REQUEST \
    --identity-source '$request.header.Authorization' \
    --name lambda-authorizer \
    --authorizer-uri "arn:aws:apigateway:$REGION:lambda:path/2015-03-31/functions/arn:aws:lambda:$REGION:$ACCOUNT:function:lambda-auth/invocations" \
    --authorizer-payload-format-version '2.0' \
    --enable-simple-responses
```
</div></details>

## lambda functionに権限追加
Amazon API Gatewayがlambda functionを実行できるように、権限を与えます。
<details><summary>lambda functionに権限追加</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ export AUTHORIZER_ID=$(aws apigatewayv2 get-authorizers --api-id $API_ID --query "Items[?Name==\`lambda-authorizer\`].AuthorizerId" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ aws lambda add-permission \
    --function-name lambda-auth \
    --statement-id apigateway-invoke-permission-lambda-authorizer \
    --action lambda:InvokeFunction \
    --principal apigateway.amazonaws.com \
    --source-arn "arn:aws:execute-api:$REGION:$ACCOUNT:$API_ID/authorizers/$AUTHORIZER_ID"
```
</div></details>

## すべてのルートにauthorizerを追加
これでAmazon API Gatewayからauthorizerのlambda functionが呼び出せるようになりましたので、各ルートにauthroizerを割り当てます。
<details><summary>すべてのルートにauthorizerを追加</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ export ROUTES=$(aws apigatewayv2 get-routes --api-id $API_ID --query "Items[].RouteId" --output text)
nmatsui@:aws-stf-core-scorpio (main =)$ for route in $ROUTES; do
  aws apigatewayv2 update-route \
  --api-id $API_ID \
  --route-id $route \
  --authorization-type CUSTOM \
  --authorizer-id $AUTHORIZER_ID
done
```
</div></details>

## トークン認証の動作確認
それでは、トークン認証が有効であることを確認します。最低限ですが、検証なのでこれでOK！
<details><summary>Authorizationヘッダ無し → 401 Unauthorized</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -i "$ENDPOINT/actuator/info"
```
```text:result
HTTP/2 401
date: Sun, 18 Sep 2022 08:40:16 GMT
content-type: application/json
content-length: 26
apigw-requestid: YpbpliAfPHcEPQw=

{"message":"Unauthorized"}
```
</div></details>
<details><summary>Authorizationヘッダ有り → 200 OK</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -i "$ENDPOINT/actuator/info" \
-H "Authorization: $TOKEN"
```
```text:result
HTTP/2 200
date: Sun, 18 Sep 2022 08:41:34 GMT
content-type: application/vnd.spring-boot.actuator.v3+json
content-length: 157
apigw-requestid: Ypb1tg7wPHcEMUw=
vary: Origin
vary: Access-Control-Request-Method
vary: Access-Control-Request-Headers
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY

{"build":{"artifact":"AllInOneRunner","name":"AllInOneRunner","time":"2022-09-08T13:51:46.913Z","version":"1.1.0-SNAPSHOT","group":"eu.neclab.ngsildbroker"}}
```
</div></details>

# ブローカー機能の確認
無事にAWS上にFIWAREのブローカー機能が立ち上がりましたので、EntityのCRUDを軽く確認してみましょう。
※ NGSI-LDの仕様に関しては、[ETSIの公式API仕様](https://www.etsi.org/deliver/etsi_gs/CIM/001_099/009/01.04.01_60/gs_cim009v010401p.pdf)や[FIWAREのStep by Step](https://ngsi-ld-tutorials.readthedocs.io/en/latest/)、[ScorpioのAPI Walkthrough](https://scorpio.readthedocs.io/en/latest/API_walkthrough.html)などを確認してください。

## contextの扱い
NGSI v2とは異なり、構造化されたデータの語彙を特定するためにNGSI-LDは`context`を要求します。このcontextは、以下の２つの方法で指定できる事に[なっています](https://ngsi-ld-tutorials.readthedocs.io/en/latest/ngsi-ld-operations.html#read-operations)。

* "Link"ヘッダでcontextを指定し、リクエストボディには何も書かない
* "Content-Type"ヘッダに`application/ld+json`を指定し、リクエストボディの`@context`でcontextを指定する

## 空の状態でEntity一覧を取得
NGSI v2とは異なり、QueryStringで`type`を指定しないと`400 Bad Request`になってしまいます。
なお本来、GETはリクエストボディが無いためLinkで指定する方法しか使えないはずですが、Scorpioは"Content-Type"ヘッダに`application/ld+json`を指定するとリクエストが通るようです。
<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d type=TempSensor \
| jq .
```
```json:response
[]
```
</div></details>
<details><summary>リクエストボディでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG $ENDPOINT/ngsi-ld/v1/entities/ \
-H "Authorization: $TOKEN" \
-H "Content-Type: application/ld+json" \
-d type=TempSensor \
| jq .
```
```json:response
[]
```
</div></details>

## Entityを登録
NGSI-LDのEntityは、NGSI v2と見た目は似ていますがidの付け方やmetadataの付け方が異なります。詳細はNGSI-LDの仕様を確認してください。とりあえず今回は、NGSI v2とほとんど同じようなデータモデルにしています。
<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -iX POST "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d @- <<EOS
{
  "id": "urn:ngsi-ld:TempSensor:001",
  "type": "TempSensor",
  "category": {
    "type": "Property",
    "value": "sensor"
  },
  "temperature": {
    "type": "Property",
    "value": 25.2,
    "unitCode": "CEL"
  }
}
EOS
```
```text:response
HTTP/2 201
date: Sun, 18 Sep 2022 11:08:57 GMT
content-length: 0
apigw-requestid: YpxbZgo0PHcEJcA=
vary: Origin
vary: Access-Control-Request-Method
vary: Access-Control-Request-Headers
location: /ngsi-ld/v1/entities/urn:ngsi-ld:TempSensor:001
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
```

</div></details>
<details><summary>リクエストボディでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -iX POST "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Content-Type: application/ld+json' \
-d @- <<EOS
{
  "id": "urn:ngsi-ld:TempSensor:002",
  "type": "TempSensor",
  "category": {
    "type": "Property",
    "value": "sensor"
  },
  "temperature": {
    "type": "Property",
    "value": 28.4,
    "unitCode": "CEL"
  },
  "@context": [
    "https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld"
  ]
}
EOS
```
```text:response
HTTP/2 201
date: Sun, 18 Sep 2022 11:10:53 GMT
content-length: 0
apigw-requestid: YpxtrhRkPHcEMCw=
vary: Origin
vary: Access-Control-Request-Method
vary: Access-Control-Request-Headers
location: /ngsi-ld/v1/entities/urn:ngsi-ld:TempSensor:002
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
```

</div></details>

## Entity一覧を取得
`/ngsi-ld/v1/entities/`をGETすることで、Entityの一覧を取得できます。本来のLinkヘッダでのcontextの指定をしていない場合、プロパティ名にcontextが入るようです。

<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d type=TempSensor \
| jq .
```
```json:response
[
  {
    "id": "urn:ngsi-ld:TempSensor:001",
    "type": "TempSensor",
    "category": {
      "type": "Property",
      "value": "sensor"
    },
    "temperature": {
      "type": "Property",
      "value": 25.2,
      "unitCode": "CEL"
    }
  },
  {
    "id": "urn:ngsi-ld:TempSensor:002",
    "type": "TempSensor",
    "category": {
      "type": "Property",
      "value": "sensor"
    },
    "temperature": {
      "type": "Property",
      "value": 28.4,
      "unitCode": "CEL"
    }
  }
]
```

</div></details>
<details><summary>リクエストボディでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H "Content-Type: application/ld+json" \
-d type=TempSensor \
| jq .
```
```json
[
  {
    "id": "urn:ngsi-ld:TempSensor:001",
    "type": "TempSensor",
    "https://smart-data-models.github.io/data-models/terms.jsonld#/definitions/category": {
      "type": "Property",
      "value": "sensor"
    },
    "https://smart-data-models.github.io/data-models/terms.jsonld#/definitions/temperature": {
      "type": "Property",
      "value": 25.2,
      "unitCode": "CEL"
    }
  },
  {
    "id": "urn:ngsi-ld:TempSensor:002",
    "type": "TempSensor",
    "https://smart-data-models.github.io/data-models/terms.jsonld#/definitions/category": {
      "type": "Property",
      "value": "sensor"
    },
    "https://smart-data-models.github.io/data-models/terms.jsonld#/definitions/temperature": {
      "type": "Property",
      "value": 28.4,
      "unitCode": "CEL"
    }
  }
]
```

</div></details>

## IDを指定してEntityを取得
NGSI v2と同様、`/ngsi-ld/v1/entities/<entity id>/`をGETすることで、そのEntityを取得することができます。この場合、QueryStringで`type`を指定する必要はありません。
<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/urn:ngsi-ld:TempSensor:001" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
| jq .
```
```json:response
{
  "id": "urn:ngsi-ld:TempSensor:001",
  "type": "TempSensor",
  "category": {
    "type": "Property",
    "value": "sensor"
  },
  "temperature": {
    "type": "Property",
    "value": 25.2,
    "unitCode": "CEL"
  }
}
```

</div></details>
<details><summary>リクエストボディでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/urn:ngsi-ld:TempSensor:002" \
-H "Authorization: $TOKEN" \
-H "Content-Type: application/ld+json" \
| jq .
```
```json:response
{
  "id": "urn:ngsi-ld:TempSensor:002",
  "type": "TempSensor",
  "https://smart-data-models.github.io/data-models/terms.jsonld#/definitions/category": {
    "type": "Property",
    "value": "sensor"
  },
  "https://smart-data-models.github.io/data-models/terms.jsonld#/definitions/temperature": {
    "type": "Property",
    "value": 28.4,
    "unitCode": "CEL"
  }
}
```
</div></details>

## Entityをフィルタ
QueryStringに`q`パラメータでフィルタ条件を書くことで、Entityをフィルタすることができます。
<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d type=TempSensor \
-d q=temperature==26..30 \
| jq .
```
```json:response
[
  {
    "id": "urn:ngsi-ld:TempSensor:002",
    "type": "TempSensor",
    "category": {
      "type": "Property",
      "value": "sensor"
    },
    "temperature": {
      "type": "Property",
      "value": 28.4,
      "unitCode": "CEL"
    }
  }
]
```
</div></details>

## 特定の属性のみ取得
QuetyStringに`attrs`パラメータを指定すると、指定した属性のみ取得することができます。
<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d type=TempSensor \
-d attrs=temperature \
| jq .
```
```json:response
[
  {
    "id": "urn:ngsi-ld:TempSensor:001",
    "type": "TempSensor",
    "temperature": {
      "type": "Property",
      "value": 25.2,
      "unitCode": "CEL"
    }
  },
  {
    "id": "urn:ngsi-ld:TempSensor:002",
    "type": "TempSensor",
    "temperature": {
      "type": "Property",
      "value": 28.4,
      "unitCode": "CEL"
    }
  }
]
```
</div></details>

## Entityを更新
更新すべき属性をボディに指定して`/ngsi-ld/v1/entities/<entity id>/`にPATCHすることで、その属性を更新することができます。この場合リクエストボディには更新すべき属性のみ記述するため、必然的にcontextは"Link"ヘッダで指定することになります。
<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -iX PATCH "$ENDPOINT/ngsi-ld/v1/entities/urn:ngsi-ld:TempSensor:002/attrs" \
-H "Authorization: $TOKEN" \
-H 'Content-Type: application/json' \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d @- <<EOS
{
  "temperature": {
    "type": "Property",
    "value": 31.2,
    "unitCode": "CEL"
  }
}
EOS
```
```text:response
HTTP/2 204
date: Sun, 18 Sep 2022 11:46:52 GMT
apigw-requestid: Yp2-5jksvHcEMQA=
vary: Origin
vary: Access-Control-Request-Method
vary: Access-Control-Request-Headers
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
```
</div></details>
<details><summary>更新したEntityを確認</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d type=TempSensor \
| jq .
```
```json:response
[
  {
    "id": "urn:ngsi-ld:TempSensor:001",
    "type": "TempSensor",
    "category": {
      "type": "Property",
      "value": "sensor"
    },
    "temperature": {
      "type": "Property",
      "value": 25.2,
      "unitCode": "CEL"
    }
  },
  {
    "id": "urn:ngsi-ld:TempSensor:002",
    "type": "TempSensor",
    "category": {
      "type": "Property",
      "value": "sensor"
    },
    "temperature": {
      "type": "Property",
      "value": 31.2,
      "unitCode": "CEL"
    }
  }
]
```
</div></details>

## Entityの削除
最後に`/ngsi-ld/v1/entities/<entity id>/`にDELETEすることで、そのEntityを削除することができます。
<details><summary>Linkヘッダでcontextを指定</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -iX DELETE "$ENDPOINT/ngsi-ld/v1/entities/urn:ngsi-ld:TempSensor:001" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json'
```
```text:response
HTTP/2 204
date: Sun, 18 Sep 2022 12:00:51 GMT
apigw-requestid: Yp5CGjrfvHcEPWA=
vary: Origin
vary: Access-Control-Request-Method
vary: Access-Control-Request-Headers
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
cache-control: no-cache, no-store, max-age=0, must-revalidate
pragma: no-cache
expires: 0
x-frame-options: DENY
```
</div></details>
<details><summary>更新したEntityを確認</summary><div>

```bash
nmatsui@:aws-stf-core-scorpio (main =)$ curl -sSG "$ENDPOINT/ngsi-ld/v1/entities/" \
-H "Authorization: $TOKEN" \
-H 'Link: <https://raw.githubusercontent.com/smart-data-models/data-models/master/context.jsonld>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json' \
-d type=TempSensor \
| jq .
```
```json:response
[
  {
    "id": "urn:ngsi-ld:TempSensor:002",
    "type": "TempSensor",
    "category": {
      "type": "Property",
      "value": "sensor"
    },
    "temperature": {
      "type": "Property",
      "value": 31.2,
      "unitCode": "CEL"
    }
  }
]
```
</div></details>

# まとめ
AWS STF（Smart Territory Framework）を用いて、AWS上にFIWARE NGSI-LD Context Broker（Scorpio）を簡単に構築することができました。
ただしNGSI−LDの検証環境として利用する場合は良いですが、本番環境として使う場合は以下のようなポイントに注意が必要となります。AWS STFをベースに、自らの状況に合わせたCDKスタックを作ると良いでしょう。

## リクエストの認証認可
AWS STFをデータ連携基盤として利用する場合、HTTPメソッドとパスごとに適切な認証認可を設定する必要があります。権限のあるユーザーの管理手順など運用負荷も含めて適切な認証認可のミドルウェアを選択し、Amazon API Gatewayと連携させるようにCDKスタックを改修する必要があるでしょう。

## Managed Serviceのリソース設定
STF CoreのCDKスタックでは、AWS Aurora Serverlessや小さめのAmazon Managed Streaming for Apache Kafkaと連携するように設定されています。検証用途では良いですが、本番環境として使う場合は性能上のボトルネックになる可能性があります。実際にどの程度の頻度のリクエストが来ているのか、来るはずなのかを監視・予測して、適切なリソースサイズに変更してください。

## 他のFIWAREコンポーネントや自作コンポーネントとの連携
実際にデータ連携基盤を構築する際には、ブローカー機能だけでなくその他の機能との連携が必要となります。例えばEntityの更新を時系列で記録する時系列DBのコンポーネントや、連携されたデータの状況を可視化するBIコンポーネント、あるいは用途に合わせて自作した特別なコンポーネントなど。それらの連携をCDKスタックに組み込む必要があるでしょう。

# そもそも・・・
デジタル田園都市国家構想でもオープンなデータ連携基盤が必要と謳われており[^3]、FIWARE orionをベースとしたブローカー機能が必要と言われています[^4][^5][^6]。そのFIWARE orionもAWS STFが採用しているscorpioも、共にFIWAREのContext Brokerコンポーネントではありますが、NGSI v2のデータモデルを扱うorionとNGSI−LDを扱うscorpio間では互換性がありません。今後データ連携基盤どうしでデータ交換をするようになった時に、困るかもしれません。

[^3]: https://www.chisou.go.jp/sousei/about/mirai/pdf/digitaldenenseidoyoukou.pdf
[^4]: https://www.chisou.go.jp/sousei/about/mirai/pdf/denenkouhukin_jissou_type23_hyoka.pdf
[^5]: https://www.chisou.go.jp/sousei/about/mirai/pdf/denenkouhukin_jissou_type23_qa1-3.pdf
[^6]: https://www.digital.go.jp/assets/contents/node/basic_page/field_ref_resources/82a1ea56-128f-4cf6-bbd5-9ef6d4b7bafc/40d603f5/20220812_policies_budget_subsidies_02.pdf