Amazon Location Servicesハンズオンが面白かった件とささやかな修正提案

#Amazon Locations Servicesハンズオンが面白かった

昨日の2021年7月17日、毎週土曜日恒例の「[AWS エバンジェリストシリーズ　AWSの基礎を学ぼう - connpass](https://awsbasics.connpass.com/)」のハンズオン「[最新サービスをみんなで触ってみる はじめての位置情報サービス - connpass](https://awsbasics.connpass.com/event/215185/)」に参加しました。

https://awsbasics.connpass.com/event/215185/

昨年発表されたAmazon Locations Servicesを触ってみようというハンズオンですが、このサービスの雰囲気は「[Amazon Location — マップと位置の認識をアプリケーションに追加 | Amazon Web Services ブログ](https://aws.amazon.com/jp/blogs/news/amazon-location-add-maps-and-location-awareness-to-your-applications/)」がよく伝えていると思います。自前のマップサービスをベースにするGoogle Mapsに対して、[Esri](https://www.esri.com/ja-jp/home)やHERE Technologyなどパートナーが提供するマップをベースに透過的に利用できる位置情報サービスにまとまっている感じで、もしかしたらOpenStreetMaps系の人が聞いたら熱いのかなとかも思ったりも。

https://aws.amazon.com/jp/blogs/news/amazon-location-add-maps-and-location-awareness-to-your-applications/

ハンズオンでは簡単なHTML上にマップを表示したり、ジオフェンスを作ってそこを人が出入りするような動作を確認したりして、こういうことがこんな簡単にできるのかと思えて面白かったです。手順書は主催者の亀田さんが作成されたもので非公開なのだけど、以下とそのリンク先AWSドキュメントを見てもらうと似たような試用体験ができそうです。

* [AWSの地図サービス、Amazon Location Service を試してみた！（２）実践編 - Geolonia blog](https://blog.geolonia.com/2021/06/07/amazon-location-service-2.html)
* [Amazon Location Service を使って GPS デバイスが特定エリアを出入りしたら通知する仕組みを作ってみた #reinvent | DevelopersIO](https://dev.classmethod.jp/articles/amazon-location-service-mqtt-notification-try/)

ハンズオンイベントでは最初にSORACOMの[松下“Max”享平さん](https://qiita.com/ma2shita)による[位置情報周りの概説](https://speakerdeck.com/soracom/introduction-to-location-positioning-basics-and-soracom-aws-integration)や、ハンズオン後の3件の「やってみたLT」もありました（一部資料は[資料ページ](https://awsbasics.connpass.com/event/215185/presentation/)で見られます）。いろいろと初心者には勉強になるイベントでした。まことに感謝。

# MQTTを利用した追跡用Lambda関数の修正提案

ハンズオンの中では「[Lambda 関数の作成 - Amazon Location Service で MQTT を使用したトラッキング - Amazon Location Service](https://docs.aws.amazon.com/ja_jp/location/latest/developerguide/tracking-using-mqtt.html#mqtt-create-lambda)」のLambda関数を使用しました（上記のDevelopersIOの記事も同様）。

このLambda関数ですが、緯度経度が不適切なデータが送られてきたなどでエラーが起きたときに、この関数自体もエラーを起こすのに気付きました。このミスは「[【ハンズオン参加レポ】「AWSの基礎を学ぼう 特別編　最新サービスをみんなで触ってみる はじめての位置情報サービス」に参加しました - Load to Professional...](https://oni-rb.hateblo.jp/entry/2021/07/18/151507#1-%E4%BD%8D%E7%BD%AE%E6%83%85%E5%A0%B1%E3%81%AE%E6%8C%87%E5%AE%9A%E6%99%82%E3%81%AE%E3%83%9F%E3%82%B9)」でも「はまったところ」に挙げられていて、何人かやってしまっていたと思います。

この時、最終行で次のようなエラーが出力され、CloudWatch LogsのこのLambda関数のロググループに記録されていました。

```
TypeError: Object of type 'datetime' is not JSON serializable
```

## 想定原因

エラーメッセージでググるとすぐに、以下の記事が見つかりました。

>こんにちは、@yoheiMuneです。Pythonのjsonモジュールを使うといい感じにjsonが扱えますが、dateやdatetimeなどのJSONに定義されていない型は、はうまく文字列に変換できません。今日はその対応Tipsをブログに書きたいと思います。
>([[Python] dateやdatetimeをjson.dumpでエラーなく出力する - YoheiM .NET](https://www.yoheim.net/blog.php?q=20170703))

まずこのLambda関数でのエラーは、`lambda_handler`末尾の以下の部分、`json.dumps`処理で発生しています。

```python
  client = boto3.client("location")
  response = client.batch_update_device_position(TrackerName=TRACKER_NAME, Updates=updates)

  return {
    "statusCode": 200,
    "body": json.dumps(response)
  }
```

この引数である`response`は`boto3.client("location").batch_update_device_position(...)`の戻り値です。この関数の戻り値は、[ドキュメントのResponse Syntax](https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/location.html#LocationService.Client.batch_update_device_position)を見ると次のようになっています。

```python
{
    'Errors': [
        {
            'DeviceId': 'string',
            'Error': {
                'Code': 'AccessDeniedError'|'ConflictError'|'InternalServerError'|'ResourceNotFoundError'|'ThrottlingError'|'ValidationError',
                'Message': 'string'
            },
            'SampleTime': datetime(2015, 1, 1)
        },
    ]
}
```

まず処理の成功時は`response`は空みたいなものですが、エラー時は`Errors`にエラー情報が含まれます。このエラー情報には`'SampleTime': datetime(2015, 1, 1)`のように`datetime`値が含まれているようです。そして上記ブログ記事によれば…

>Pythonのjsonモジュールを使うといい感じにjsonが扱えますが、dateやdatetimeなどのJSONに定義されていない型は、はうまく文字列に変換できません。(略)JSONに日付（date/datetime）の型が定義されていないことが原因で、そのためjsonモジュールはどのように出力すれば良いのかわからず、エラーとなってしまいます。

…という状況になります。`boto3.client("location").batch_update_device_position(...)`の処理が成功している正常系ではこの問題は起きず、失敗した時だけさらにこのエラーが発生してエラー情報がログに記録されないので、ちょっと調査に困りました。

## 解消方法

上記記事を参考に、次のようにLambda関数を書きかえたら、問題は解消しました。

```python
from datetime import date, datetime
import json
import os

import boto3

# Update this to match the name of your Tracker resource
TRACKER_NAME = "MyTracker"

"""
This Lambda function receives a payload from AWS IoT Core and publishes device updates to Amazon Location Service via the BatchUpdateDevicePosition API.

Parameter 'event' is the payload delivered from AWS IoT Core.

In this sample, we assume that the payload has a single top-level key 'payload' and a nested key
'location' with keys 'lat' and 'long'. We also assume that the name of the device is nested in
the payload as 'deviceid'. Finally, the timestamp of the payload is present as 'timestamp'. For
example:

>>> event
{ 'payload': { 'deviceid': 'thing123', 'timestamp': 1604940328,
  'location': { 'lat': 49.2819, 'long': -123.1187 } } }

If your data doesn't match this schema, you can either use the AWS IoT Core rules engine to
format the data before delivering it to this Lambda function, or you can modify the code below to
match it.
"""

def json_serial(obj):
  # see also: https://www.yoheim.net/blog.php?q=20170703
  if isinstance(obj, (datetime, date)):
    return obj.isoformat()
  raise TypeError ("Type %s not serializable" % type(obj))

def lambda_handler(event, context):
  # loads the side-loaded Amazon Location Service model
  os.environ["AWS_DATA_PATH"] = os.environ["LAMBDA_TASK_ROOT"]

  updates = [
    {
      "DeviceId": event["payload"]["deviceid"],
      "SampleTime": datetime.fromtimestamp(event["payload"]["timestamp"]).isoformat(),
      "Position": [
        event["payload"]["location"]["long"],
        event["payload"]["location"]["lat"]
      ]
    }
  ]

  client = boto3.client("location")
  response = client.batch_update_device_position(TrackerName=TRACKER_NAME, Updates=updates)

  return {
    "statusCode": 200,
    "body": json.dumps(response, default=json_serial)
  }
```

ほぼ同内容を[GISTに置いてある](https://gist.github.com/mtsukamoto/1b172967a7971d26a7ea2a7ded181645)ので、コードだけほしいときはそちらからお持ちいただいてもいいと思います。

## 修正提案 

…ということで修正提案を、大本のコンテンツと思われる「[Lambda 関数の作成 - Amazon Location Service で MQTT を使用したトラッキング - Amazon Location Service](https://docs.aws.amazon.com/ja_jp/location/latest/developerguide/tracking-using-mqtt.html#mqtt-create-lambda)」に修正提案をしてみたいと思います。たぶん以下のページからフィードバックを送るのだと思うのだけど、フォームを見る限り英語だし、修正内容と目的を各1,000文字に収める感じだし、初めてなのでうまくできるかな。

https://docs-feedback.aws.amazon.com/feedback.jsp?hidden_service_name=Amazon%20Location&topic_url=http://docs.aws.amazon.com/ja_jp/location/latest/developerguide/tracking-using-mqtt.html

まずは情報整理と思って、このQiita投稿を作成してみました。この先は生暖かく見守っていただければと思います。

# 参考

* [最新サービスをみんなで触ってみる はじめての位置情報サービス - connpass](https://awsbasics.connpass.com/event/215185/)
* [【ハンズオン参加レポ】「AWSの基礎を学ぼう 特別編　最新サービスをみんなで触ってみる はじめての位置情報サービス」に参加しました - Load to Professional...](https://oni-rb.hateblo.jp/entry/2021/07/18/151507)
* [Lambda 関数の作成 - Amazon Location Service で MQTT を使用したトラッキング - Amazon Location Service](https://docs.aws.amazon.com/ja_jp/location/latest/developerguide/tracking-using-mqtt.html#mqtt-create-lambda)
* [[Python] dateやdatetimeをjson.dumpでエラーなく出力する - YoheiM .NET](https://www.yoheim.net/blog.php?q=20170703)