samを使った体重管理
概要
SAMを学んだので形あるものを作ってみた記事になります。
筆者は学生の頃スポーツをやっていたのですが、チームでは(身長-100)kgの体重が目標として課せられていました。(身長175cmなら目標75kg)
そのときの集計を自動化してみました。
完成物はこちら。
環境
- windows11
- sam-cli(1.78.0)
- aws-cli(2.9.10)
- docker(20.10.23)
手動集計フロー
- 月に一度、集計担当がメンバーに体重測定を依頼
- メンバーは測定結果の写真をポジションのリーダーに提出
- 各リーダーは測定結果をLINE(テキスト形式)で集計担当に提出
- 集計担当は各リーダーから提出されたものをExcelに打ち込み、メンバーに共有
AWSサービスでの集計
今回は上記に示したフローを下記のようにAWSのサービスを使って自動化しました。
- 集計担当がメンバーの氏名・メールアドレスが記載されたcsvをs3にアップロードする(CLI or コンソールから)
- S3のPUTイベントをトリガーにlambdaを起動(read-csv)
- csvから氏名・メールアドレスを読み込み、Dynamoに登録
- 読み込んだメールアドレスをメッセージに詰めSQSに送信
- SQSイベントをトリガーにLambdaを起動(send-mail)
- S3にホスティングした体重入力ページの署名URLを生成
- 署名URLを本文に含め、各メールアドレスにSESを利用してメール送信
- 各メンバーは体重入力ページで体重・身長を入力する(save-data)
- コールしたAPIで体重・身長をDynamoに登録する
- 毎月末、CloudWatch EventをトリガーにLamndaを起動(write-csv)
- Dynamoのデータを集計し、CSVに書き込む
- 集計データ用のバケットにCSVをアップロード
実装
read-csvを実装する
read-csvは集計フローの2の処理を担う関数です。
read-csv関数のソースコードはこちら
下記3つのAWSリソースが必要になります。
- CSVをアップロードするs3バケット
- CSVから読み込んだアドレスを書き込むDynamoのテーブル
- CSVから読み込んだアドレスを送信するSQSキュー
作成したs3バケットへのファイルアップロードをトリガーにLambda関数を実行します。
ソースコード中で、SQS,DynamoDBのテーブル名を参照する必要があるため、生成した各リソース名を環境変数に定義しています。
Lambdaには各AWSリソースにアクセスするための権限も忘れずに付与する必要があります。
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
SAM Template for weight-management
Parameters:
MembersTableName:
Type: String
CSVBucketName:
Type: String
Globals:
Function:
Timeout: 5
Environment:
Variables:
ADMIN_MAIL_ADDRESS: !Ref AdminMailAddress
MEMBER_TABLE: !Ref MembersTableName
QUEUE_URL: !Ref SendQueue
REGION: 'ap-northeast-1'
HOSTING_BUCKET: !Ref HostingBucketName
Resources:
CSVUploadBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref CSVBucketName
SendQueue:
Type: AWS::SQS::Queue
MemberTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref MembersTableName
AttributeDefinitions:
- AttributeName: email
AttributeType: S
KeySchema:
- AttributeName: email
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 3
WriteCapacityUnits: 3
ReadCsvFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: read-csv/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
- SQSSendMessagePolicy:
QueueName: !GetAtt SendQueue.QueueName
- S3ReadPolicy:
BucketName: !Ref CSVBucketName
Events:
BodyUploadEvent:
Type: S3
Properties:
Bucket: !Ref CSVUploadBucket
Events: s3:ObjectCreated:*
Outputs:
ReadCsvFunction:
Description: 'ReadCsvFunction ARN'
Value: !GetAtt ReadCsvFunction.Arn
SendQueue:
Description: 'SendQueue ARN'
Value: !GetAtt SendQueue.Arn
send-mailを実装する
send-mailは集計フローの3の処理を担う関数です。
send-mail関数のソースコードはこちら
今回はSESの集計担当のメールアドレス検証はAWSCLIを使って行いました。
email={集計担当のメールアドレス}
aws ses verify-email-identity --email-address $email
メールの本文には、体重入力ページの署名付きURLを埋め込みます。
read-csv実装時に作成したリソースに加え、体重入力ページをホスティングするs3バケットが必要になります。
PublicAccessBlockはオンにするべきと考えましたが、そうするとバケットポリシーの設定がうまくいきませんでした。今回はtemplate.ymlではPublicAccessBlockはオフにし、デプロイ後にオンとする方針をとっています。
HostingBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref HostingBucketName
OwnershipControls:
Rules:
- ObjectOwnership: ObjectWriter
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
WebsiteConfiguration:
IndexDocument: index.html
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Id: S3MyPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref HostingBucket
- /*
Bucket: !Ref HostingBucket
SendMailFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: send-mail/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
- SESCrudPolicy:
IdentityName: !Ref AdminMailAddress
Events:
SQSReceiveEvent:
Type: SQS
Properties:
Queue: !GetAtt SendQueue.Arn
BatchSize: 10
作成後にパブリックアクセスをブロックします
aws s3api put-public-access-block \
--bucket {バケット名} \
--public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,Bl
ockPublicPolicy=true,RestrictPublicBuckets=true"
入力ページはメールアドレス・体重・身長を入力するだけの簡易的なページです。
ここまでを作成した上でメールアドレスが記載されたcsvをアップロードすると、下記のメールがcsvに記載のあるアドレスに送信されます。
email,name
hoge@example.com,hoge
hoga@example.com,hoga
save-dataを実装する
save-dataは集計フローの4の処理を担う関数です。
save-data関数のソースコードはこちらです。
体重入力ページからコールされるAPIを作成します。
下記のようなリクエストボディを受け取り、DynamoDbのテーブルを更新します。
{
"email":"hoge@example.com",
"height":175,
"weight":75
}
今回はドメインの取得等は行っていないため、自動生成されるエンドポイントをindex.html中のscriptにべた書きしてしまっています。
SaveDataAPI:
Type: AWS::Serverless::Api
Properties:
StageName: stg
Cors:
AllowMethods: "'POST,OPTIONS'"
AllowHeaders: "'Content-Type'"
AllowOrigin: "'*'"
MaxAge: "'600'"
SaveDataFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: save-data/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
Events:
PostWeight:
Type: Api
Properties:
Path: /weight
Method: POST
RestApiId: !Ref SaveDataAPI
write-csvを実装する
write-csvは集計フローの5の処理を担う関数です。
write-csv関数のソースコードはこちら
集計後のcsvファイルをアップロードするためのS3バケット作成のために、template.ymlを修正します。
write-csvはCloudWatch Eventを使って、毎月末の定期実行としています。
write-csvが実行されると集計後のcsvがアップロードされます。
WriteCsvFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: write-csv/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
- S3WritePolicy:
BucketName: !Ref CalculateCsvBucketName
Events:
SeduleCalculate:
Type: Schedule
Properties:
Schedule: cron(0 15 L * ? *) # 毎月末00:00に実行
email,name,Weight,Height
hoge@example.com,hoge,65,165
hoge@exapmle.com,hoga,75,175
template.ymlの最終系
最終的にtemplate.ymlは下記ののようになりました
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
weight-management
Sample SAM Template for weight-management
Parameters:
AdminMailAddress:
Type: String
MembersTableName:
Type: String
CSVBucketName:
Type: String
HostingBucketName:
Type: String
CalculateCsvBucketName:
Type: String
Globals:
Function:
Timeout: 5
Environment:
Variables:
ADMIN_MAIL_ADDRESS: !Ref AdminMailAddress
MEMBER_TABLE: !Ref MembersTableName
QUEUE_URL: !Ref SendQueue
REGION: 'ap-northeast-1'
HOSTING_BUCKET: !Ref HostingBucketName
CALCULATE_BUCKET: !Ref CalculateCsvBucketName
Resources:
CSVUploadBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref CSVBucketName
SendQueue:
Type: AWS::SQS::Queue
MemberTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: !Ref MembersTableName
AttributeDefinitions:
- AttributeName: email
AttributeType: S
KeySchema:
- AttributeName: email
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 3
WriteCapacityUnits: 3
HostingBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref HostingBucketName
OwnershipControls:
Rules:
- ObjectOwnership: ObjectWriter
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
WebsiteConfiguration:
IndexDocument: index.html
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
PolicyDocument:
Id: S3MyPolicy
Version: 2012-10-17
Statement:
- Sid: PublicReadForGetBucketObjects
Effect: Allow
Principal: '*'
Action: 's3:GetObject'
Resource: !Join
- ''
- - 'arn:aws:s3:::'
- !Ref HostingBucket
- /*
Bucket: !Ref HostingBucket
CalculateCsvBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref CalculateCsvBucketName
ReadCsvFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: read-csv/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
- SQSSendMessagePolicy:
QueueName: !GetAtt SendQueue.QueueName
- S3ReadPolicy:
BucketName: !Ref CSVBucketName
Events:
BodyUploadEvent:
Type: S3
Properties:
Bucket: !Ref CSVUploadBucket
Events: s3:ObjectCreated:*
SendMailFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: send-mail/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
- SESCrudPolicy:
IdentityName: !Ref AdminMailAddress
Events:
SQSReceiveEvent:
Type: SQS
Properties:
Queue: !GetAtt SendQueue.Arn
BatchSize: 10
SaveDataAPI:
Type: AWS::Serverless::Api
Properties:
StageName: stg
Cors:
AllowMethods: "'POST,OPTIONS'"
AllowHeaders: "'Content-Type'"
AllowOrigin: "'*'"
MaxAge: "'600'"
SaveDataFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: save-data/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
Events:
PostWeight:
Type: Api
Properties:
Path: /weight
Method: POST
RestApiId: !Ref SaveDataAPI
WriteCsvFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: write-csv/
Handler: main
Runtime: go1.x
Architectures:
- x86_64
Tracing: Active
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref MembersTableName
- S3WritePolicy:
BucketName: !Ref CalculateCsvBucketName
Events:
SeduleCalculate:
Type: Schedule
Properties:
Schedule: cron(0 15 L * ? *) # 毎月末00:00に実行
Outputs:
ReadCsvFunction:
Description: 'ReadCsvFunction ARN'
Value: !GetAtt ReadCsvFunction.Arn
SaveDataFunction:
Description: 'SaveDataFunction ARN'
Value: !GetAtt SaveDataFunction.Arn
終わり
SAMを触り初めてみた初心者がとりあえず動くものを作ってみました。
機能・構成に諸々の突っ込みどころはあると思いますがいい勉強になりました。
やっぱり自分で動かしてみるのが大事。
参考
Discussion