🔖

samを使った体重管理

2023/05/08に公開

概要

SAMを学んだので形あるものを作ってみた記事になります。
筆者は学生の頃スポーツをやっていたのですが、チームでは(身長-100)kgの体重が目標として課せられていました。(身長175cmなら目標75kg)
そのときの集計を自動化してみました。

完成物はこちら。
https://github.com/HiromuSaito/weight-managemnt

環境

  • windows11
  • sam-cli(1.78.0)
  • aws-cli(2.9.10)
  • docker(20.10.23)

手動集計フロー

  • 月に一度、集計担当がメンバーに体重測定を依頼
  • メンバーは測定結果の写真をポジションのリーダーに提出
  • 各リーダーは測定結果をLINE(テキスト形式)で集計担当に提出
  • 集計担当は各リーダーから提出されたものをExcelに打ち込み、メンバーに共有
    old_flow.png

AWSサービスでの集計

今回は上記に示したフローを下記のようにAWSのサービスを使って自動化しました。

  1. 集計担当がメンバーの氏名・メールアドレスが記載されたcsvをs3にアップロードする(CLI or コンソールから)
  2. S3のPUTイベントをトリガーにlambdaを起動(read-csv)
    1. csvから氏名・メールアドレスを読み込み、Dynamoに登録
    2. 読み込んだメールアドレスをメッセージに詰めSQSに送信
  3. SQSイベントをトリガーにLambdaを起動(send-mail)
    1. S3にホスティングした体重入力ページの署名URLを生成
    2. 署名URLを本文に含め、各メールアドレスにSESを利用してメール送信
  4. 各メンバーは体重入力ページで体重・身長を入力する(save-data)
    1. コールしたAPIで体重・身長をDynamoに登録する
  5. 毎月末、CloudWatch EventをトリガーにLamndaを起動(write-csv)
    1. Dynamoのデータを集計し、CSVに書き込む
    2. 集計データ用のバケットにCSVをアップロード

architecture.png

実装

read-csvを実装する

read-csvは集計フローの2の処理を担う関数です。
read-csv関数のソースコードはこちら

下記3つのAWSリソースが必要になります。

  • CSVをアップロードするs3バケット
  • CSVから読み込んだアドレスを書き込むDynamoのテーブル
  • CSVから読み込んだアドレスを送信するSQSキュー

作成したs3バケットへのファイルアップロードをトリガーにLambda関数を実行します。
ソースコード中で、SQS,DynamoDBのテーブル名を参照する必要があるため、生成した各リソース名を環境変数に定義しています。
Lambdaには各AWSリソースにアクセスするための権限も忘れずに付与する必要があります。

template.yml
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はオフにし、デプロイ後にオンとする方針をとっています。

template.yml
  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に記載のあるアドレスに送信されます。

resource.csv
email,name
hoge@example.com,hoge
hoga@example.com,hoga

save-dataを実装する

save-dataは集計フローの4の処理を担う関数です。
save-data関数のソースコードはこちらです。

体重入力ページからコールされるAPIを作成します。
下記のようなリクエストボディを受け取り、DynamoDbのテーブルを更新します。

request.json
{
	"email":"hoge@example.com",
	"height":175,
	"weight":75
}

今回はドメインの取得等は行っていないため、自動生成されるエンドポイントをindex.html中のscriptにべた書きしてしまっています。

template.yml
  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がアップロードされます。

template.yml
  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に実行
集計後のcsv
email,name,Weight,Height
hoge@example.com,hoge,65,165
hoge@exapmle.com,hoga,75,175

template.ymlの最終系

最終的に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を触り初めてみた初心者がとりあえず動くものを作ってみました。
機能・構成に諸々の突っ込みどころはあると思いますがいい勉強になりました。
やっぱり自分で動かしてみるのが大事。

参考

https://book.impress.co.jp/books/1121101032
https://awstip.com/aws-breaking-change-news-new-s3-buckets-blocked-for-public-access-ad83d626afb4

Discussion