Owntracks の位置情報を AWS IoT にMQTT連携し Timestream に緯度・経度をストアする




## はじめに
AWS IoT と、Timestream の学習＆アウトプットになります。
簡単に作り直せるように AWS CLI で構築しています。

2021年4月現在 Timestream は東京リージョンにローンチされていないので Lambda を利用してデータ転送しています。

## 構成
![qiita-f52c0841775f9fc4d0c5-owntracks.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58181/d101459b-8e93-670a-b30e-8957e4c483cf.png)




## 利用環境
* AWS CLI: aws-cli/2.1.18 Python/3.7.9 Windows/10 exe/AMD64 prompt/off
* AWS SAM CLI: version 1.22.0
* AWS IoT Core
* AWS Timestream
* Owntracks (Android): Version 2.3.0 (23024)
* Windows 10 Pro: 20H2
* Git Bash for Windows (AWS CLI実行ターミナル): 2.28.0.windows.1
* Windows PowerShell: 5.1.19041.868


## 構築フロー
* 1. AWS環境変数セット
* 2. AWS IoT Core 構築
  * 2-1. AWS IoT core > ACT > ルール 作成
     * S3バケット作成
     * Timestream 構築
          * Lambda ビルド＆デプロイ
     * ルールのロール作成
     * ルール作成
  * 2-2. AWS IoT core > 安全性 > 証明書 作成
     * 証明書、秘密鍵、公開鍵の作成
     * PKCS #12 個人情報交換ファイル（p12）作成 
  * 2-3. AWS IoT core > 安全性 > ポリシー 作成
  * 2-4. AWS IoT core > 管理 > モノ 作成
  * 2-5. IOT Core の ATS エンドポイント（ホスト）取得
* 3. Owntracks 設定 
* 4. AWS Timestream 構築
  * 4-1. Lambda ビルド＆デプロイ
  * 4-2. AWS IoT core > ACT > ルール 追加


## 1. AWS環境変数、作業変数の設定
ここでは kikudai プロファイル、リージョンを環境変数にセットして利用しますが、defaultのプロファイルを利用する場合は設定不要です。

```bash
export AWS_PROFILE=kikudai
export AWS_DEFAULT_REGION=ap-northeast-1
bucket_name="qiita-f52c0841775f9fc4d0c5-owntracks"
common_name="qiita_f52c0841775f9fc4d0c5_owntracks"
mkdir ~/${common_name}
cd ~/${common_name}

```

## 2. AWS IoT core の構築

### 2-1. AWS IoT core > ACT > ルール 作成
Owntracks テレメトリデータ（MQTT Publish データ）を S3 に連携するルールとロールを作成します。

この S3 連携の主な目的はテレメトリデータのバックアップ、デバック、リストアになります。
後述の Lambda による Timestream 連携ルールの設定の方で、Timestream にデータ（緯度・経度など）をストアします。

#### S3 バケット作成
Owntracks テレメトリデータを保存する S3 バケットを作成します。

```bash:S3バケット作成
aws s3 mb "s3://${bucket_name}"

```

#### Timestream 構築
Owntracks のテレメトリデータ（MQTT Publish メッセージ）を AWS Lambda アクションで Timestream に連携します。
[Lambda サンプルCode（Python）](https://github.com/kikudai/qiita_f52c0841775f9fc4d0c5_owntracks/blob/main/qiita_f52c0841775f9fc4d0c5_owntracks/app.py)

##### Lambda ビルド＆デプロイ
SAM cli で Lambda をビルド、デプロイします。

> Windows の場合、
pip install -U aws-sam-cli
で sam cli をインストールするのが一番手っ取り早いと思います。

```bash:Lambdaビルド＆デプロイ
git clone https://github.com/kikudai/qiita_f52c0841775f9fc4d0c5_owntracks.git
cd qiita_f52c0841775f9fc4d0c5_owntracks

sam build

# 対話形式のデプロイ
sam deploy --guided | tee deploy.log

```

sam ddeploy --guided を実行すると下記のような対話形式となるので [XXXX]: はエンター、 Y/n は一律 y を入力します。 

```:対話形式の入力例
Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Found
        Reading default arguments  :  Success
  
        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [to-timestream]: エンター
        AWS Region [ap-northeast-1]: エンター
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [Y/n]: yエンター
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: yエンター
        Save arguments to configuration file [Y/n]: yエンター
        SAM configuration file [samconfig.toml]: エンター
        SAM configuration environment [default]: エンター

        Looking for resources needed for deployment: Not found.
        Creating the required resources...
        Successfully created!
...

Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]:
yエンター
...

```

> ローカル docker で動作確認したい場合は以下のコマンド
> 
```bash:ローカルdockerによる起動テスト
sam build --use-container
sam local invoke OwntracksFunction --event /events/event_is_u.json
```
ただ、このテスト event.json が tst (Timestream に書き込むときの Time）のままだと、Timestream のメモリストアの期限に（今回は1時間としてます）ヒットして、 RejectedRecordsException が発生するはずなので、 sed などで今のUNIX時間に書き換えてご利用ください。
>
```:sedでUNIX時間を書換サンプル
# 本当はこういう感じでファイルを書き換えることなく、 sed を利用したいところです
sam local invoke OwntracksFunction \
  --event <(sed -e "/tst/ s/[0-9]+/$(date +'%s')/g" ./events/event_is_u.json)
>
# ただし、Git Bash for Windows では上記のような bash のプロセス置換がうまくいかず
# 普通にファイルを書き換えました
sed -ie --regexp-extended "/tst/ s/[0-9]+/$(date +'%s')/g" ./events/event.json
```

#### 参考
* [AWS SAM CLI 再入門 2020.07 - Qiita](https://qiita.com/hayao_k/items/7827c3778a23c514e196)

#### ルールのロール作成
__AWS IoT core > ACT > ルール__ のロールを作成します。
また、 AWS 管理ポリシーで用意されている次の Iot 関連ポリシー（ [IAM ポリシー - AWS IoT Core](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iam-policies.html) ）をアタッチします。

* AWSIoTThingsRegistration
* AWSIoTLogging
* AWSIoTRuleActions

> Git Bash for Windows による aws cli 実行で `file://` は、カレントのファイルでないと読み込めないようです。
例えば、こういったことができません。
>> echo 'hoge' > /tmp/hoge.json
>> aws iot hoge --hogehoge file:///tmp/hoge.json

>（Windows のリアルパスを指定すると可能かもしれません）

```bash:ルールのロール作成
cat <<'EOF' > ${common_name}_iot_role.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "iot.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

aws iam create-role \
  --role-name ${common_name}_iot_role \
  --assume-role-policy-document file://${common_name}_iot_role.json

aws iam attach-role-policy \
  --role-name ${common_name}_iot_role \
  --policy-arn "arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration"

aws iam attach-role-policy \
  --role-name ${common_name}_iot_role \
  --policy-arn "arn:aws:iam::aws:policy/service-role/AWSIoTLogging"

aws iam attach-role-policy \
  --role-name ${common_name}_iot_role \
  --policy-arn "arn:aws:iam::aws:policy/service-role/AWSIoTRuleActions"

```

#### ルール作成
__AWS IoT core > ACT > ルール__ を作成します。
--topic-rule-payload の sql の FROM 句 'owntracks/#'` は Owntracksアプリで設定されるTopicを指定しています。
この指定で、Owntracks のすべてテレメトリを取得するようにします。

> Lambda のトリガーを aws cli で追加するコマンドがなかなか探し出せず時間がかかりました。
トリガー追加の cli コマンドは `lambda add-permission` になります。
コンソール画面だと、ルールのアクション追加でさりげなく Lambda のトリガーも追加されますが、 aws cli の場合は、 `iot create-topic-rule` で作成しただけでは lambda トリガーの設定がされません。
また、 lambda トリガーの状態確認は以下になります。
>
```:Lambdaトリガー状態確認のサンプル
aws lambda get-policy --function-name ${lambda}) | jq -r .Policy | jq .
```
このことから lambda トリガーはポリシーとして設定されているようですね。

```bash:ルール作成
arn_iot_role=$(
  aws iam get-role \
  --role-name ${common_name}_iot_role \
  | jq .Role.Arn
)

lambda_arn=$(aws lambda list-functions \
  | jq -rc .Functions[].FunctionArn \
  | grep to-timestream-OwntracksFunction
)

cat <<EOF > ${common_name}_iot_s3_and_timestream_rule.json
{
  "sql": "SELECT clientid() AS clientid, timestamp() AS event_t, * FROM 'owntracks/#'",
  "ruleDisabled": false,
  "awsIotSqlVersion": "2016-03-23",
  "actions": [
    {
      "lambda": {
        "functionArn": "${lambda_arn}"
      }
    },
    {
      "s3": {
        "roleArn": ${arn_iot_role},
        "bucketName": "${bucket_name}",
        "key": "\${clientId()}/\${timestamp()}.json"
       }
    }
  ]
}
EOF

aws iot create-topic-rule \
  --rule-name ${common_name}_topic_rule \
  --topic-rule-payload  file://${common_name}_iot_s3_and_timestream_rule.json

lambda=$(aws lambda list-functions \
  | jq -rc .Functions[].FunctionName \
  | grep to-timestream-OwntracksFunction
)

source_arn=$(aws iot get-topic-rule \
  --rule-name ${common_name}_topic_rule \
  | jq -r .ruleArn)

# 素直に上記 source_arn で利用した aws iot get-topic-rule の結果から
# Account 部分を切り取ったほうがいいケースがありそう
source_account=$(aws sts get-caller-identity | jq -r .Account)

aws lambda add-permission \
  --function-name ${lambda} \
  --principal iot.amazonaws.com \
  --source-arn "${source_arn}" \
  --action "lambda:InvokeFunction" \
  --statement-id ${common_name}_topic_rule \
  --source-account ${source_account}

```

##### 参考情報
* [JSON - OwnTracks Booklet](https://owntracks.org/booklet/tech/json/#topics)
* [FROM 句 - AWS IoT Core](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/iot-sql-from.html)
* [【小ネタ】AWS CLIでAWS Account IDが取れるようになりました！ | DevelopersIO](https://dev.classmethod.jp/articles/get-aws-account-id-with-get-caller-identity/)
* [CodeCommitのLambdaトリガーを作るときはパーミッションをお忘れなく - Qiita](https://qiita.com/shikazuki/items/3aa96c132f151da4b708)
* [Lambda - AWS IoT Core](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/lambda-rule-action.html)


### 2-2. AWS IoT core > 安全性 > 証明書 作成

#### 証明書、秘密鍵、公開鍵 作成

```bash:証明書、秘密鍵、公開鍵作成
aws iot create-keys-and-certificate \
  --certificate-pem-outfile "${common_name}.cert.pem" \
  --public-key-outfile "${common_name}.public.key" \
  --private-key-outfile "${common_name}.private.key" \
  | tee "${common_name}_certificate.json"

```

#### 証明書のアクティブ化
作成した証明書を有効化します。

```bash:証明書のアクティブ化
aws iot update-certificate \
  --certificate-id $(
    cat ${common_name}_certificate.json | jq -r .certificateId
  ) \
  --new-status ACTIVE

```

#### 個人情報交換ファイル（PKCS #12）作成
Owntracks では拡張子が p12 の PKCS #12 個人情報交換ファイル必要なため、 openssl コマンド を利用し上記で取得した証明書と秘密鍵とパスワードから p12 ファイルを作成します。


> Git Bash for Windows で openssl コマンドを実行すると Git Bash が固まったので、 PowerShell で実行しました。

`Enter Export Password:` とパスワードを求められるので（確認パスワードも）入力します。
このパスワードは Owntracks アプリで証明書として p12 ファイルを登録するとき必要になります。

```powershell:個人情報交換ファイル（PKCS#12）作成
# PowerShellによる実行
$common_name = "qiita_f52c0841775f9fc4d0c5_owntracks"
cd ~/${common_name}
openssl pkcs12 `
  -export `
  -in "${common_name}.cert.pem" `
  -inkey "${common_name}.private.key" `
  -out "${common_name}.identity.p12"

```

##### 参考情報
* [PKCS #12 個人情報交換ファイルフォーマットについて - Qiita](https://qiita.com/kunichiko/items/3e2ec27928a95630a73a)


#### CA 証明書取得（サーバー認証用）
Owntracks アプリの証明書設定で必要な CA 証明書を取得します。

> URLリンク切れの場合は [サーバー認証 - AWS IoT Core](https://docs.aws.amazon.com/ja_jp/iot/latest/developerguide/server-authentication.html) や [aws iot ca証明書 - Google 検索](https://www.google.com/search?q=aws+iot+ca%E8%A8%BC%E6%98%8E%E6%9B%B8&oq=aws+&aqs=edge.0.69i59j69i60l2j69i59l2j69i60l2.3961j0j1&sourceid=chrome&ie=UTF-8) で、探してください。
また、Windows10 や Android に既に設定されている Amazon CA証明書 があれば、それでも利用できると思います。


```bash:CA証明書取得（サーバー認証用）
curl -O https://www.amazontrust.com/repository/AmazonRootCA1.pem

```


### 2-3. AWS IoT core > 安全性 > ポリシー 作成

ポリシーを作成し、そのポリシーを証明書にアタッチします。

```bash:ポリシー作成
cat <<EOF > ${common_name}_policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Publish",
        "iot:Subscribe",
        "iot:Receive",
        "iot:Connect"
      ],
      "Resource": [
        "*"
      ]
    }
  ]
}
EOF

aws iot create-policy \
  --policy-name ${common_name}_policy \
  --policy-document file://${common_name}_policy.json

aws iot attach-policy \
  --policy-name ${common_name}_policy \
  --target $(
    cat ${common_name}_certificate.json | jq -r .certificateArn
  )

```

### 2-4. AWS IoT core > 管理 > モノ 作成
モノを作成し、証明書をアタッチします。

```bash:モノ作成
aws iot create-thing \
  --thing-name ${common_name}_android

aws iot attach-thing-principal \
  --thing-name ${common_name}_android \
  --principal $(
    cat ${common_name}_certificate.json | jq -r .certificateArn
  )

```

### 2-5. IOT Core の ATS エンドポイント（ホスト）取得
Owntracks のホストに設定するATSエンドポイントを取得します。

```bash:ATSエンドポイント（ホスト）取得
aws iot describe-endpoint \
    --endpoint-type iot:Data-ATS

```

#### 参考情報
* [AWS IoT Core に Amazon Trust Sevices (ATS) の署名付き証明書が提供される新しいエンドポイントが追加され、Symantec が信頼されなくなることに伴う問題を回避](https://aws.amazon.com/jp/about-aws/whats-new/2018/08/aws-iot-core-adds-new-endpoints-serving-amazon-trust-services-signed-certificates-to-help-customers-avoid-symantec-distrust-issues/)

### Owntracks の設定
カレントディレクトリにある、以下のファイルを Google Drive などに保存します。

* `AmazonRootCA1.pem`
* `qiita_f52c0841775f9fc4d0c5_owntracks.identity.p12`

#### Connection > Mode 設定
* MQTT に設定します

#### Connection > Host 設定
* `Host` に *2-5. IOT Core の ATS エンドポイント（ホスト）取得* の ホスト を設定します

> endpointAddress で取得できた値の `xxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com` を設定します。
>
```:ATSエンドポイント取得結果サンプル
{
    "endpointAddress": "xxxxxxxxxxxxxx-ats.iot.ap-northeast-1.amazonaws.com"
}

* `Port` を `8883` に設定します
* `Client ID` に 適当な ID を設定します  
（自動で設定されていればそのまま利用で問題ありません）

> IoT ルール作成で設定した sql 
`"SELECT clientid() AS clientid, timestamp() AS timestamp, * FROM 'owntracks/#'"`
の `clientid()` で取得できる値になります。 

![owntracks_host_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58181/a594b1a8-04f9-767c-dc3a-1d85155045af.jpeg)


#### Connection > Security 設定
* `TLS` をオンにします
* `CA certificate` に Google Drive などに保存した `AmazonRootCA1.pem` を選択します
* `Client certificate` に Google Drive などに保存した `qiita_f52c0841775f9fc4d0c5_owntracks.identity.p12` を選択します
* `Client certificate password` に p12ファイルを設定したときの パスワードを入力します

![owntracks_security_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58181/68ae2323-1e7b-5988-6dc8-e563a1fa6f8a.jpeg)

#### Owntracks MQTT 接続確認
画面右上の 丸 i マーク を押して Status を確認します。
![owntracks_i_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58181/3b902f1e-8f02-1642-ac61-ccc8251dbf72.jpeg)

Endpoint state が Connected になっていれば、問題なく接続されています。
Error になっている場合は 設定の何かで不足か間違いがあります。
![awntracks_status_s.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58181/b8d416b4-566d-765f-13cf-d2d6e35ab1ae.jpeg)

ここまでの設定が問題なければ、 Timestream 米国東部 (バージニア北部: us-east-1) にデータが書き込まれ、 S3 のバケット qiita-f52c0841775f9fc4d0c5-owntracks に Owntracks からのテレメトリデータ（json）が格納されていきます。


### クリーニング（削除）

#### 削除のための環境変数、変数設定
```bash
export AWS_PROFILE=kikudai
export AWS_DEFAULT_REGION=ap-northeast-1
bucket_name="qiita-f52c0841775f9fc4d0c5-owntracks"
common_name="qiita_f52c0841775f9fc4d0c5_owntracks"
cd ~/${common_name}

```

#### S3バケット削除
```bash
aws s3 rb s3://${bucket_name} \
  --force

```

#### IoTルールのロール削除
```bash
aws iam detach-role-policy \
  --role-name ${common_name}_iot_role \
  --policy-arn "arn:aws:iam::aws:policy/service-role/AWSIoTThingsRegistration"

aws iam detach-role-policy \
  --role-name ${common_name}_iot_role \
  --policy-arn "arn:aws:iam::aws:policy/service-role/AWSIoTLogging"

aws iam detach-role-policy \
  --role-name ${common_name}_iot_role \
  --policy-arn "arn:aws:iam::aws:policy/service-role/AWSIoTRuleActions"

aws iam delete-role \
  --role-name ${common_name}_iot_role 

```

#### IoTルールの削除
```bash
aws iot delete-topic-rule \
  --rule-name ${common_name}_topic_rule 

```

#### IoTポリシー削除
```bash
aws iot delete-policy \
  --policy-name ${common_name}_policy

```

#### IoTモノ削除
```bash
aws iot delete-thing \
  --thing-name ${common_name}_android

```

#### IoT証明書削除
```bash
aws iot update-certificate \
  --certificate-id $(
    cat ${common_name}_certificate.json | jq -r .certificateId
  ) \
  --new-status INACTIVE

aws iot delete-certificate \
  --certificate-id $(
    cat ${common_name}_certificate.json | jq -r .certificateId
  )

```

#### 作業ディレクトリの削除
```bash
cd
rm -rf ~/${common_name}

```

#### CloudFormation スタック削除
SAM CLI のデプロイ時に利用したスタックとLambdaの削除

> aws-sam-cli-managed-default というスタックも作成されますが、他の方が SAM CLIを利用していた場合は削除しないほうが無難です。
S3バケット、aws-sam-cli-managed-default-samclisourcebucket-XXXXXXXXも同様です。
削除しても問題ない場合は、S3バケットを空にしてから
aws cloudformation delete-stack --stack-name aws-sam-cli-managed-default
してください。


```bash
aws cloudformation delete-stack --stack-name to-timestream
```

#### Timestream 削除

```bash
aws timestream-write delete-table \
  --region us-east-1 \
  --database-name ${common_name} \
  --table-name location

aws timestream-write delete-table \
  --region us-east-1 \
  --database-name ${common_name} \
  --table-name lwt

aws timestream-write delete-database \
  --region us-east-1 \
  --database-name ${common_name}
```


### 参考情報
* [iot — AWS CLI 1.19.47 Command Reference](https://docs.aws.amazon.com/cli/latest/reference/iot/)
* [RSA鍵、証明書のファイルフォーマットについて - Qiita](https://qiita.com/kunichiko/items/12cbccaadcbf41c72735)
* [JSONの中に文字列として記載されたJSONの値をjqで扱ってみる | DevelopersIO](https://dev.classmethod.jp/articles/json-in-json-string-use-jq/)