前書き
これまでは私はクラウド(AWS)と通信するデバイス側の開発(組込みデバイス、デスクトップアプリ)が中心でした。I/Fの仕様が決まれば通信はできますが、サービスを作る上でクラウド側の知識がないとエンジニアとして面白みがないなと感じることが増えてきました。これは現業が類似した業務の繰り返しでエンジニアとしてこのままで良いのかという危機感的な部分もありますが、スキルを増やしたいというモチベーションが強いです。
個人的な経験からクラウドの運用にはコストはかかりますが、アイデアを具現化することに関して、短期的に拡張性を考慮して実現することに利点があると考えています。何だかんだ見栄えも良いです。また、その中心はクラウドという印象です。いくらデバイスが良くても、データを取得、分析、解析までできなければ、使えるサービスにはなりません。各工程のスキルを身に付けることは簡単なことではありませんが、作りたいものの選択肢は拡がると思います。
一方で、ちょうど業務でAWSのサービスを使うことになり、良い機会なので、プライベートでもアカウントも作成して触りながら知識を深めていこうと思い、コードを書いてAWSのサービスを使ってみることにしました。
前置きが長くなりましたが、本記事はクラウドを触ってみた記事であり、AWSのサービスとして、API Gateway, Lambda, DynamoDBを使用した記事になります。
前提条件
- AWSアカウント(ルートユーザー)を作成済みであること
ソースコードはGitHubにコミットしています。
github.com
IAMユーザー作成
ルートユーザーでログインし、「IAMユーザーを作成する 」を指定して作成します。
ステップ 1 ユーザーの詳細を指定
- ユーザー名:[割愛]
- AWS マネジメントコンソールへのユーザーアクセスを提供する - オプション:チェック
- IAMユーザーを作成します:選択
- コンソールパスワード
- ユーザーは次回のサインイン時に新しいパスワードを作成する必要があります - 推奨:チェック
「ユーザーは次回のサインイン時に新しいパスワードを作成する必要があります」という項目については、チェックをつけます。
自動作成だと文字数が少ないため、ログイン時に変更した方が良いと思いました。
また、作成後は参照できませんが、作成直後はコピー、csv保存が可能です。
補足: IAMユーザー作成時の選択肢
chatgptより引用(2023/11/05時点)
AWSのIAMユーザー作成時には、以下の2つの選択肢があります。
- Identity Centerでユーザーを指定する - Identity Centerでは、AWSアカウントおよびクラウドアプリケーションへのユーザーアクセスを一元管理できます。これは推奨される方法です。
- IAMユーザーを作成する - アクセスキー、AWS CodeCommit または Amazon Keyspaces のサービス固有の認証情報、または緊急アカウントアクセス用のバックアップ認証情報を使用してプログラムによるアクセスを有効にする必要がある場合のみ、IAMユーザーの作成が推奨されます。
一般的には、Identity Centerでユーザーを指定することが推奨されています。これにより、AWSアカウントおよびクラウドアプリケーションへのユーザーアクセスを一元管理できます。ただし、プログラムによるアクセスを有効にする必要がある場合は、IAMユーザーの作成を選択することもあります。どちらの選択肢を選ぶかは要件によります。
また、「Identity Centerでユーザーを指定する」場合は、AWS Organizationsを有効化する必要があります。AWS Organizationsは、複数のAWSアカウントを一元管理するためのサービスです。これにより、単一の支払い者と一元化されたコスト追跡が可能になり、他のAWSアカウントを作成して招待したり、ポリシーベースのコントロールを適用したりすることができます。
今回は個人利用で複数のAWSアカウントを管理する必要がないため、「IAMユーザーを作成します」で作成しました。
ステップ 2 許可を設定
- 許可のオプション:ポリシーを直接アタッチする(
AdministratorAccess
を指定)
ステップ 3 確認して作成
ユーザーの詳細、許可の概要、タグ(オプション)を確認し、問題がなければ、ユーザーの作成を押下する。
タグ(オプション)は必要に応じて設定する。
ステップ 4 パスワードを取得
パスワード情報をコピー、csv保存し、完了
参考記事
IAMユーザーのアクセスキー作成
AWSの各サービスを利用するためにはアクセスキーが必要になります。アカウント作成時はアクセスキーを自動で作成する選択肢もあったかと思いますが、基本的にはIAMユーザー作成後にアクセスキーを作成します。以下の指定で作成します。
IAMユーザー > セキュリティ認証情報 > アクセスキーを作成 > ユーザーケース選択(※) > 次へ
※AWS CLI V2で認証情報を設定、コードで参照する方式のため、コマンドラインインターフェース(CLI)を選択しました。
DynamoDB
テーブルの作成
以下で作成します。その他はデフォルトです。
- テーブル名:
TestJsonTable1
- パーテーションキー:
id
- ソートキー:
date
補足:予約語について
ソートキーで指定するdate
は予約語です。
DynamoDB の予約語 - Amazon DynamoDB
もし、AWSCLIで取得する場合は#
をつけて実行する必要があるようです。
今回作成するLambda関数(Python)ではquery
メソッドを使用しますが、書式は変更しなくても取得出来ました。
DynamoDB の式の属性名 - Amazon DynamoDB
データの追加
pythonコードでjsonファイルを読み込み、テーブルに追加します。
エラーにならなければ"Data written to table successfully."が表示されます。
追加したデータはテーブル > 名前 > 項目の検索から確認できます。
例えば、クエリ、id:camera2
、date:次の値/2023-04-01および2023-04-2
で実行すれば条件に応じたデータを表示できます。
テストデータ(test_data.json)を作成するコード
注意点
- テストデータは正確ではありません。あくまでサンプルです。
- データとしては特定のカメラで検出したPoseNetの骨格点情報を持つ日時データ(json)を想定しています。本来は検出時間間隔は短いのですが、サンプルのため30分間隔にしています。
skeletal_points
は都合により検出時間で共通としています。
create_json_file.py
import json
import random
from datetime import datetime, timedelta
skeletal_points = '[{"key_point": "2", "key_points": [{"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}]}, {"key_point": "3", "key_points": [{"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}, {"prob": "83.89", "x": "464", "y": "115"}]}]'
start_time = datetime(2023, 4, 1)
end_time = datetime(2023, 4, 5)
time_step = timedelta(minutes=30)
results = []
while start_time <= end_time:
result = {
"detected_date_time": start_time.strftime("%Y-%m-%dT%H:%M:%S.000"),
"date": start_time.strftime("%Y-%m-%dT%H:%M:%S.000"),
"detected_status": random.choice(["fallen", "normal"]),
"id": random.choice(["camera1", "camera2"]),
"skeletal_points": skeletal_points,
"num_of_people": str(random.randint(0, 5)),
}
results.append(result)
start_time += time_step
with open("./incert_files/test_data.json", "w") as f:
json.dump({"result": results}, f, indent=2)
test_data.jsonを書き込むコード
incert_data_json.py
import json
import boto3
dynamodb = boto3.resource(
"dynamodb",
endpoint_url="https://dynamodb.ap-northeast-1.amazonaws.com",
)
table_name = "TestJsonTable1"
table = dynamodb.Table(table_name)
file_name = "./incert_files/test_data.json"
with open(file_name, "r") as f:
data = json.load(f)
for item in data["result"]:
table.put_item(Item=item)
print("Data written to table successfully.")
GETメソッド 経由でLambda関数を実行するため、API GatewayでAPIを作成します。
- 新しいAPI
- API名:TestJsonTable1_API1
- APIエンドポイント:リージョン
メソッドを作成
- メソッドタイプ: GET
- 統合タイプ:Lambda 関数
- Lambdaプロキシ:オフ
- Lambda関数:後述するLambda関数のARNを指定
メソッドリクエストの設定
URL クエリ文字列パラメータ
メソッドリクエストを編集 > URL クエリ文字列パラメータ
- id:必須(True)
- start_date:必須(False)
- end_date:必須(False)
統合リクエストの設定
{
"id": "$input.params('id')",
"start_date": "$input.params('start_date')",
"end_date": "$input.params('end_date')"
}
マッピングテンプレートを設定する理由
URLクエリのみ設定すると以下のエラーになりました。
{'errorMessage': "'id'", 'errorType': 'KeyError', 'requestId': '[割愛]', 'stackTrace': [' File "/var/task/lambda_function.py", line 17, in lambda_handler\n id = event[\'id\']\n']}
chatgptで確認(2023/11/05時点)するとGETリクエストで指定したパラメータはLambda関数ではイベントオブジェクトでは参照できないようです。
これはエラーメッセージ'errorMessage': "'id'", 'errorType': 'KeyError'は、Pythonの辞書(この場合はevent)から存在しないキー(この場合は'id')を取得したことによるエラーです。
API Gateway経由でLambda関数を呼び出すとき、API GatewayはHTTPリクエストを特定の形式のイベントオブジェクトに変換します。このイベントオブジェクトは、Lambda関数のeventパラメータとして渡されます。
しかし、このイベントオブジェクトは、直接Lambda関数をテストするときに使用するイベントオブジェクトとは形式が異なる場合があります。そのため、Lambda関数がevent['id']を期待している場合でも、API Gatewayから渡されるイベントオブジェクトには'id'キーが含まれていない可能性があります。
この問題を解決するためには、API Gatewayの統合リクエストのマッピングテンプレートを設定して、HTTPリクエストから適切な形式のイベントオブジェクトを作成する必要があります。具体的には、マッピングテンプレートでHTTPリクエストのクエリパラメータ(この場合はid、start_date、end_date)をイベントオブジェクトの対応するキーにマッピングします。
これにより、Lambda関数はAPI Gatewayから期待する形式のイベントオブジェクトを受け取り、'id'キーを正しく取得できるようになります。
Lambda
GETリクエストのパラメータを元にDynamoDBから取得するクエリを設定、実行するLambda関数を作成します。
関数の作成
- 関数名:
getItemKpsTestTableJson1
- ランタイム:
Python 3.11
- アーキテクチャ:
x86_64
アーキテクチャではx86_64
とarm64
を指定できますが、デフォルトで選択されていたx86_64
を指定しました。
下記記事を見る限りは、処理速度、コスト面でarm64
を選択するのが良さそうです。
参考記事
Lambdaのポリシー変更(追加)
IAM > アクセス管理 > ロール > Lambdaのロール名
dynamodbにアクセスするため、AmazonDynamoDBFullAccess
を追加する
デフォルトではCloudWatchのログ出力するAWSLambdaBasicExecutionRole
が設定されている。
レイヤーの追加
pandas
を使用するためPython 3.11
版のarn
を指定して追加します。
参考記事: [AWS]ARNとは?
pandasのarn情報
arnとして以下を指定します。
arn:aws:lambda:ap-northeast-1:336392948345:layer:AWSSDKPandas-Python311:2
参考情報: AWS Lambda Managed Layers — AWS SDK for pandas 3.4.2 documentation
Lambda関数作成
関数 > 関数名 > コード
import boto3
import pandas as pd
from boto3.dynamodb.conditions import Key
def lambda_handler(event, context):
dynamodb = boto3.resource(
"dynamodb",
region_name="ap-northeast-1",
endpoint_url="https://dynamodb.ap-northeast-1.amazonaws.com",
)
table = dynamodb.Table('TestJsonTable1')
id = event['id']
start_date = event.get('start_date')
end_date = event.get('end_date')
if start_date and end_date:
response = table.query(
KeyConditionExpression=Key('id').eq(id) & Key('date').between(start_date, end_date)
)
else:
response = table.query(
KeyConditionExpression=Key('id').eq(id)
)
return {"result": response['Items']}
実行結果次第でタイムアウト時間を伸ばす場合は以下で変更します。
関数 > 関数名 > 設定 > 一般設定 > 編集 > タイムアウト(変更) > 保存
3秒から30秒に変更しました。
テスト
Lambda関数が期待通り動作するかテストデータを使用して動作確認します。
ログから期待通り出力できていればOKです。
{
"id": "camera1",
"start_date": "2023-04-01T00:00:00.000",
"end_date": "2023-04-05T00:00:00.000"
}
実行結果
{
"result": [
{
"detected_date_time": "2023-04-01T00:00:00.000",
"date": "2023-04-01T00:00:00.000",
"detected_status": "normal",
"id": "camera1",
"skeletal_points": "[{\"key_point\": \"2\", \"key_points\": [{\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}]}, {\"key_point\": \"3\", \"key_points\": [{\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}]}]",
"num_of_people": "0"
},
...
{
"detected_date_time": "2023-04-05T00:00:00.000",
"date": "2023-04-05T00:00:00.000",
"detected_status": "fallen",
"id": "camera1",
"skeletal_points": "[{\"key_point\": \"2\", \"key_points\": [{\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}]}, {\"key_point\": \"3\", \"key_points\": [{\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}, {\"prob\": \"83.89\", \"x\": \"464\", \"y\": \"115\"}]}]",
"num_of_people": "0"
}
]
}
リクエスト関数
ここまでで、DynamoDB,API Gateway, Lambda関数を作成し、設定することができたのでローカル環境からデータを取得できるか確認します。
動作確認用にrequest
モジュールを使用してGETリクエストを送信するコードを作成します。受信したjsonデータはpandas / dataframe
に変換して保存します。
事前準備
参考記事
request_api.py
import requests
from requests_aws4auth import AWS4Auth
from typing import Optional
import pandas as pd
import boto3
session = boto3.Session()
credentials = session.get_credentials()
aws_access_key_id = credentials.access_key
aws_secret_access_key = credentials.secret_key
region_name = session.region_name
api_endpoint = "ステージ込みのエンドポイントを指定する"
auth = AWS4Auth(aws_access_key_id, aws_secret_access_key, region_name, "execute-api")
def get_req(
id: str,
columns: list,
start_date: Optional[str] = None,
end_date: Optional[str] = None,
):
url = f"{api_endpoint}?id={id}"
if start_date is not None:
url += f"&start_date={start_date}"
if end_date is not None:
url += f"&end_date={end_date}"
response = requests.get(url, auth=auth)
data = response.json()
new_data = [[item[column] for column in columns] for item in data["result"]]
df = pd.DataFrame(new_data, columns=columns)
return df
if __name__ == "__main__":
columns = ["id", "date", "detected_status", "num_of_people","skeletal_points"]
df = get_req("camera1", columns, "2023-04-01", "2023-04-02")
print(df)
実行結果
指定範囲(日付)とcolumns
のリストに対応したDataframe
オブジェクトを作成できています。
id date detected_status num_of_people skeletal_points
0 camera1 2023-04-01T00:00:00.000 normal 0 [{"key_point": "2", "key_points": [{"prob": "8...
1 camera1 2023-04-01T01:30:00.000 fallen 1 [{"key_point": "2", "key_points": [{"prob": "8...
2 camera1 2023-04-01T02:00:00.000 fallen 2 [{"key_point": "2", "key_points": [{"prob": "8...
3 camera1 2023-04-01T04:00:00.000 fallen 0 [{"key_point": "2", "key_points": [{"prob": "8...
4 camera1 2023-04-01T05:00:00.000 normal 4 [{"key_point": "2", "key_points": [{"prob": "8...
5 camera1 2023-04-01T06:30:00.000 fallen 0 [{"key_point": "2", "key_points": [{"prob": "8...
6 camera1 2023-04-01T08:00:00.000 fallen 0 [{"key_point": "2", "key_points": [{"prob": "8...
7 camera1 2023-04-01T09:30:00.000 fallen 5 [{"key_point": "2", "key_points": [{"prob": "8...
8 camera1 2023-04-01T10:00:00.000 normal 2 [{"key_point": "2", "key_points": [{"prob": "8...
9 camera1 2023-04-01T10:30:00.000 fallen 2 [{"key_point": "2", "key_points": [{"prob": "8...
10 camera1 2023-04-01T11:00:00.000 fallen 3 [{"key_point": "2", "key_points": [{"prob": "8...
11 camera1 2023-04-01T13:30:00.000 normal 3 [{"key_point": "2", "key_points": [{"prob": "8...
12 camera1 2023-04-01T15:00:00.000 normal 3 [{"key_point": "2", "key_points": [{"prob": "8...
13 camera1 2023-04-01T16:00:00.000 normal 1 [{"key_point": "2", "key_points": [{"prob": "8...
14 camera1 2023-04-01T17:00:00.000 fallen 5 [{"key_point": "2", "key_points": [{"prob": "8...
15 camera1 2023-04-01T18:00:00.000 fallen 5 [{"key_point": "2", "key_points": [{"prob": "8...
16 camera1 2023-04-01T19:30:00.000 fallen 4 [{"key_point": "2", "key_points": [{"prob": "8...
17 camera1 2023-04-01T21:00:00.000 fallen 2 [{"key_point": "2", "key_points": [{"prob": "8...
18 camera1 2023-04-01T23:30:00.000 normal 4 [{"key_point": "2", "key_points": [{"prob": "8...
まとめ
API Gateway + Lambda + DynamoDB構成でGETリクエストでDBのデータを取得して、DataFrameを作成する方法について紹介しました。
実際にサービスを使うことで触りの部分は理解できました。ただ、認証やネットワーク設定はわからない点が多いので、今後は本やドキュメントを見ながら理解を深めていきたいと思います。
以上です。