はじめに
この記事は下記記事の続きとなっていますので先にこちらをご覧ください。
本記事ではDynamoDBに蓄えたセンサーデータをReactApp(WEBアプリ)で可視化できるようにします。
構成
図のようにDynamoDBよりセンサーデータを取得するためにAPI Gateway、取得したデータの可視化にS3ホスティングされたReactAppを構築します。
API用Lambda関数
API用Lambda関数のコードとrequirements.txtをSAMプロジェクトのapi_srcディレクトリ内に配置します。
import os
from typing import Literal
from decimal import Decimal
import boto3
from mangum import Mangum
from fastapi import FastAPI
from pydantic import BaseModel
from botocore.config import Config
from boto3.dynamodb.types import TypeDeserializer
class SensorDataItemSchema(BaseModel):
Timestamp: str
Value: Decimal
dynamodb_client = boto3.client(
"dynamodb",
config=Config(
retries={
"max_attempts": 5,
"mode": "standard"
}
)
)
table_arn: str = os.environ["TABLE_ARN"]
api: FastAPI = FastAPI()
@api.get("/{sensor_type}/data", response_model=list[SensorDataItemSchema])
def get_sensor_data(sensor_type: Literal["concentration", "temperature","humidity","pressure"]):
response: dict = dynamodb_client.query(**{
"TableName": table_arn,
"KeyConditionExpression": "#Type = :type",
"ExpressionAttributeNames": {
"#Type": "Type"
},
"ExpressionAttributeValues": {
":type": {"S": sensor_type}
}
})
items: list[SensorDataItemSchema] = [
SensorDataItemSchema(**{
k: TypeDeserializer().deserialize(v)
for k, v in item.items()
}
)
for item in response.get("Items", [])
]
return items
lambda_handler: Mangum = Mangum(api)
annotated-types==0.7.0
anyio==4.6.2.post1
exceptiongroup==1.2.2
fastapi==0.115.5
idna==3.10
mangum==0.19.0
pydantic==2.10.2
pydantic_core==2.27.1
sniffio==1.3.1
starlette==0.41.3
typing_extensions==4.12.2
可視化ダッシュボード用ReactAppの構築
-
create-react-app
コマンドでReactAppを作成します>npx create-react-app env-data-viewer --template typescript
-
npm install
コマンドで必要なライブラリ(axios、MaterialUI関連、date-fns)をインストールします>npm install axios >npm install @mui/material @emotion/react @emotion/styled @mui/icons-material @mui/x-charts >npm install date-fns
-
App.tsx
を次のように書き換えますApp.tsximport axios from 'axios'; import { format } from 'date-fns'; import { Grid2, Typography } from '@mui/material'; import { useState, useEffect, type FC } from 'react'; import { axisClasses } from '@mui/x-charts/ChartsAxis'; import { LineChart, type ShowMarkParams } from '@mui/x-charts'; interface SensorDataItemSchema { Timestamp: Date; Value: number; } const getSensorData = async (sensorType: string): Promise<SensorDataItemSchema[]> => { const url = `${process.env.REACT_APP_API_ENDPOINT}/${sensorType}/data`; try { const response = await axios.get(url); return response.data.map((item: {Timestamp: string, Value: string}) => ({ Timestamp: new Date(item.Timestamp), Value: parseFloat(item.Value) })) } catch (error) { console.log("API Fail, reason:", error); return []; } } const dateFormatter = (value: Date) => { return format(value, "dd日 HH:mm"); } const showMark = (params: ShowMarkParams) => { const { position } = params as ShowMarkParams<Date>; return [0, 6, 12, 18].includes(position.getHours()) && position.getMinutes() === 0 }; const SensorDataChart: FC<{ sensorType: string, displayName: string, unit: string}> = ({ sensorType, displayName, unit }): JSX.Element => { const [ sensorData, setSensorData ] = useState<SensorDataItemSchema[]>([]); const fetchSensorData = async () => { const response = await getSensorData(sensorType); setSensorData(response); } useEffect(() => { fetchSensorData(); }, []); return ( <Grid2 size={{ xs: 12, lg: 6 }} > <Typography> {displayName} </Typography> <LineChart xAxis={[{ data: sensorData.map((item) => (item.Timestamp)), scaleType: "time", label: "日時", valueFormatter: (value) => dateFormatter(value), tickInterval: (time) => time.getMinutes() === 0, tickLabelInterval: (time) => time.getMinutes() === 0 }]} yAxis={[{ label: `${displayName} (${unit})` }]} series={[{ data: sensorData.map((item) => (item.Value)), valueFormatter: (value) => `${value} ${unit}`, showMark }]} grid={{horizontal: true}} height={400} margin={{ left: 96 }} sx={{ [`& .${axisClasses.left} .${axisClasses.label}`]: { transform: 'translateX(-32px)', } }} /> </Grid2> ) } const App: FC = (): JSX.Element => { return ( <Grid2 container spacing={2} sx={{ padding: 2 }} > <SensorDataChart sensorType="temperature" displayName="温度" unit="℃" /> <SensorDataChart sensorType="concentration" displayName="CO2濃度" unit="ppm" /> <SensorDataChart sensorType="humidity" displayName="湿度" unit="%" /> <SensorDataChart sensorType="pressure" displayName="気圧" unit="hPa" /> </Grid2> ) } export default App;
SAMによるアプリケーションのデプロイ
SAMを使用してAPI Gateway、API用Lambda関数、S3バケットを追加でデプロイします。
- template.yamlを次のように修正します
template.yaml
AWSTemplateFormatVersion: "2010-09-09" Transform: "AWS::Serverless-2016-10-31" Description: "EnvDataStack" Resources: EnvDataTable: # データ格納用DynamoDBテーブル Type: "AWS::DynamoDB::Table" Properties: AttributeDefinitions: - AttributeName: "Type" AttributeType: "S" - AttributeName: "Timestamp" AttributeType: "S" BillingMode: "PROVISIONED" DeletionProtectionEnabled: true KeySchema: - AttributeName: "Type" KeyType: "HASH" - AttributeName: "Timestamp" KeyType: "RANGE" ProvisionedThroughput: ReadCapacityUnits: 1 WriteCapacityUnits: 1 TableName: "EnvDataTable" TimeToLiveSpecification: AttributeName: "TTL" Enabled: true EnvDataPusher: # データ格納用Lambda関数 Type: "AWS::Serverless::Function" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" AutoPublishAlias: "Alias" CodeUri: "src/" Description: "iot-env-data-collectorのデバイスシャドウ更新に応じて最新のデータをDynamoDBにプッシュする関数" Environment: Variables: TABLE_ARN: !GetAtt "EnvDataTable.Arn" Events: ShadowUpdateAccepted: Type: "IoTRule" Properties: Sql: "SELECT * FROM \"$aws/things/iot-env-data-collector/shadow/update/accepted\"" FunctionName: "EnvDataPusher" Handler: "lambda_function.lambda_handler" MemorySize: 128 Policies: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*" - Effect: "Allow" Action: - "dynamodb:PutItem" Resource: !GetAtt "EnvDataTable.Arn" Runtime: "python3.9" Timeout: 30 EnvDataViewerBucket: # 可視化ダッシュボードS3ホスティングバケット Type: "AWS::S3::Bucket" Properties: BucketName: "env-data-viewer" PublicAccessBlockConfiguration: BlockPublicAcls: false BlockPublicPolicy: false IgnorePublicAcls: false RestrictPublicBuckets: false WebsiteConfiguration: IndexDocument: "index.html" EnvDataViewerBucketPolicy: # バケットポリシー Type: "AWS::S3::BucketPolicy" Properties: Bucket: !Ref "EnvDataViewerBucket" PolicyDocument: Version: "2012-10-17" Statement: - Action: "s3:GetObject" Effect: "Allow" Resource: !Sub - "${arn}/*" - arn: !GetAtt "EnvDataViewerBucket.Arn" Principal: "*" EnvDataHTTPAPI: # データ取得用HTTP API Type: "AWS::Serverless::HttpApi" Properties: CorsConfiguration: AllowCredentials: false AllowHeaders: - "content-type" AllowMethods: - "*" AllowOrigins: - !GetAtt "EnvDataViewerBucket.WebsiteURL" MaxAge: 0 Description: "センサーデータ取得用HTTP API" Name: "EnvDataHTTPAPI" EnvDataHTTPAPILambda: # データ取得用HTTP API Lambda関数 Type: "AWS::Serverless::Function" Properties: AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: - "lambda.amazonaws.com" Action: - "sts:AssumeRole" AutoPublishAlias: "Alias" CodeUri: "api_src/" Description: "センサーデータ取得用HTTP API Lambda関数" Environment: Variables: TABLE_ARN: !GetAtt "EnvDataTable.Arn" Events: GetSensorData: Type: "HttpApi" Properties: ApiId: !Ref "EnvDataHTTPAPI" Method: "GET" Path: "/{sensor_type}/data" FunctionName: "EnvDataHTTPAPI" Handler: "lambda_function.lambda_handler" MemorySize: 128 Policies: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - "logs:CreateLogGroup" - "logs:CreateLogStream" - "logs:PutLogEvents" Resource: "*" - Effect: "Allow" Action: - "dynamodb:Query" Resource: !GetAtt "EnvDataTable.Arn" Runtime: "python3.9" Timeout: 30
-
sam build
でデプロイの準備をします[EnvDataStack]$ sam build Starting Build use cache Building codeuri: /tmp/EnvDataStack/src runtime: python3.9 architecture: x86_64 functions: EnvDataPusher Manifest file is changed (new hash: df68a8bca38f0002a68e9384d43588c6) or dependency folder (.aws-sam/deps/420c2445-345f-4edd-bd82-288634f1fd0e) is missing for (EnvDataHTTPAPILambda), downloading dependencies and copying/building source Building codeuri: /tmp/EnvDataStack/api_src runtime: python3.9 architecture: x86_64 functions: EnvDataHTTPAPILambda requirements.txt file not found. Continuing the build without dependencies. Running PythonPipBuilder:CopySource Running PythonPipBuilder:CleanUp Running PythonPipBuilder:ResolveDependencies Running PythonPipBuilder:CopySource Running PythonPipBuilder:CopySource Build Succeeded Built Artifacts : .aws-sam/build Built Template : .aws-sam/build/template.yaml Commands you can use next ========================= [*] Validate SAM template: sam validate [*] Invoke Function: sam local invoke [*] Test Function in the Cloud: sam sync --stack-name {{stack-name}} --watch [*] Deploy: sam deploy --guided
-
sam deploy
でスタックを更新します[EnvDataStack]$ sam deploy ~省略~ Previewing CloudFormation changeset before deployment ====================================================== Deploy this changeset? [y/N]: y ~省略~ CloudFormation events from stack operations (refresh every 5.0 seconds) --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ResourceStatus ResourceType LogicalResourceId ResourceStatusReason --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- UPDATE_IN_PROGRESS AWS::CloudFormation::Stack EnvDataStack User Initiated CREATE_IN_PROGRESS AWS::S3::Bucket EnvDataViewerBucket - CREATE_IN_PROGRESS AWS::IAM::Role EnvDataHTTPAPILambdaRole - CREATE_IN_PROGRESS AWS::S3::Bucket EnvDataViewerBucket Resource creation Initiated CREATE_IN_PROGRESS AWS::IAM::Role EnvDataHTTPAPILambdaRole Resource creation Initiated CREATE_COMPLETE AWS::S3::Bucket EnvDataViewerBucket - CREATE_IN_PROGRESS AWS::S3::BucketPolicy EnvDataViewerBucketPolicy - CREATE_IN_PROGRESS AWS::S3::BucketPolicy EnvDataViewerBucketPolicy Resource creation Initiated CREATE_COMPLETE AWS::S3::BucketPolicy EnvDataViewerBucketPolicy - CREATE_COMPLETE AWS::IAM::Role EnvDataHTTPAPILambdaRole - CREATE_IN_PROGRESS AWS::Lambda::Function EnvDataHTTPAPILambda - CREATE_IN_PROGRESS AWS::Lambda::Function EnvDataHTTPAPILambda Resource creation Initiated CREATE_COMPLETE AWS::Lambda::Function EnvDataHTTPAPILambda - CREATE_IN_PROGRESS AWS::Lambda::Version EnvDataHTTPAPILambdaVersion758a4c335e - CREATE_IN_PROGRESS AWS::Lambda::Version EnvDataHTTPAPILambdaVersion758a4c335e Resource creation Initiated CREATE_COMPLETE AWS::Lambda::Version EnvDataHTTPAPILambdaVersion758a4c335e - CREATE_IN_PROGRESS AWS::Lambda::Alias EnvDataHTTPAPILambdaAliasAlias - CREATE_IN_PROGRESS AWS::Lambda::Alias EnvDataHTTPAPILambdaAliasAlias Resource creation Initiated CREATE_COMPLETE AWS::Lambda::Alias EnvDataHTTPAPILambdaAliasAlias - CREATE_IN_PROGRESS AWS::ApiGatewayV2::Api EnvDataHTTPAPI - CREATE_IN_PROGRESS AWS::ApiGatewayV2::Api EnvDataHTTPAPI Resource creation Initiated CREATE_COMPLETE AWS::ApiGatewayV2::Api EnvDataHTTPAPI - CREATE_IN_PROGRESS AWS::ApiGatewayV2::Stage EnvDataHTTPAPIApiGatewayDefaultStage - CREATE_IN_PROGRESS AWS::Lambda::Permission EnvDataHTTPAPILambdaGetSensorDataPermission - CREATE_IN_PROGRESS AWS::Lambda::Permission EnvDataHTTPAPILambdaGetSensorDataPermission Resource creation Initiated CREATE_COMPLETE AWS::Lambda::Permission EnvDataHTTPAPILambdaGetSensorDataPermission - CREATE_IN_PROGRESS AWS::ApiGatewayV2::Stage EnvDataHTTPAPIApiGatewayDefaultStage Resource creation Initiated CREATE_COMPLETE AWS::ApiGatewayV2::Stage EnvDataHTTPAPIApiGatewayDefaultStage - UPDATE_COMPLETE_CLEANUP_IN_PROGRESS AWS::CloudFormation::Stack EnvDataStack - UPDATE_COMPLETE AWS::CloudFormation::Stack EnvDataStack - --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- Successfully created/updated stack - EnvDataStack in ap-northeast-1
ReactAppをビルドしS3バケットに配置する
-
ReactAppの
.env
ファイルに作成されたAPI Gatewayのエンドポイントを記述します.envREACT_APP_API_ENDPOINT=https://8XXXXXXXXl.execute-api.ap-northeast-1.amazonaws.com
-
npm run build
コマンドでReactAppをビルドします>npm run build
-
ビルドされた
build
フォルダ内のファイル・フォルダ一式を作成されたS3バケットにアップロードします
可視化ダッシュボードの完成
S3バケットのバケットウェブサイトエンドポイントを開くと可視化ダッシュボードが表示され、温度湿度気圧CO2のセンサーデータが時系列データとして確認することができます。
さいごに
IoT Coreに送信されたセンサーデータをDynamoDBに蓄え、ReactAppから蓄えたデータをAPIで取得しグラフィカルに表示できるようになりました。
SSL化やCognitoを利用したユーザ認証など付け加えたいものはありますが、一連の記事はここまでにすることにします。
記事中端折った部分も多分にありますが誰かのIoTデバイス作りの参考になれば幸いです。