APIGateway + Lambda + DynamoDB を利用したサーバレス構築ハンズオン

はじめに

普段DynamoDBをあまり使用しないので、CFnの練習がてら構築していきたいと思います。
前回のブログでは、CLIからDynamoDBに値を渡していましたが、いっそのことフロント画面から送り保存すれば、より学習が捗るかと思いまして着手してみました。

といってもフロント画面などさっぱりなので、前回に引き続き学習教本として、AWS Lambda実践ガイド 第2版 (impress top gear) 著:大澤文孝
を利用しています。
GUI及び、SAMによる構築はありましたが、CFnでの構築はなかったので、今回も書籍のエッセンスを使いつつ、自分なりに少しアーキテクトを変更して構築をしてみました。


構成図


ハンズオン

構築のながれ

1.APIGateway作成:HTMLフォームからの値を受取り、Lambdaを呼び出すため構築

2.Lambda作成:値を取得してDynamoDBに登録するため構築

3.DynamoDB作成:値を DB(テーブル)に登録するため構築

4.S3作成:APIGateWayを呼び出すHTMLフォーム保存のため構築


1.Lambda作成:値を取得してDynamoDBに登録するため構築

・手順4で構築するHTMLに入力された値を受け取り、手順3で構築するDynamoDBのテーブルに登録する。
・登録する際'username' : {'S': username}で、DynamoDBへ型の指定を行っている。

AWSTemplateFormatVersion: '2010-09-09'
Description:
  Lambda Create
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "Lambda Configuration"
        Parameters:
        - FunctionName
        - Description
        - Handler
        - MemorySize
        - Runtime
        - Timeout
        - TagsName

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  FunctionName:
    Type: String
    Default: "cfn-lmd-writedynamodb-inamura"
  Description:
    Type: String
    Default: "cfn-lmd-writedynamodb-inamura"
  Handler:
    Type: String
    Default: "index.lambda_handler"
  MemorySize:
    Type: String
    Default: "128"
  Runtime:
    Type: String
    Default: "python3.9"
  Timeout:
    Type: String
    Default: "10"
  TagsName:
    Type: String
    Default: "inamura"
# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  Lambda
# ------------------------------------------------------------#
  Lambda:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |
          import base64
          import json
          import boto3

          table_name = 'mailaddress'

          #DynamoDBオブジェクト
          dynamodb = boto3.client('dynamodb')

          def lambda_handler(event, context):
            try:
              #フォームの入力データを得る
              body = event['body']
              if event['isBase64Encoded']:
                body = base64.b64decode(body)

              decoded = json.loads(body)
              username = decoded['username']
              email = decoded['email']
              notsend = decoded['notsend']
              send = decoded['send']

              #mailaddressテーブルに登録する
              item = {
                'username' : {'S': username},
                'email' : {'S': email},
                'notsend' : {'N': notsend},
                'send' : {'N': send}
              } 

              dynamodb.put_item(TableName=table_name, Item=item)

              #結果を返す
              return json.dumps({})

            except:
            #エラーメッセージを返す
              import traceback
              err = traceback.format_exc()
              print(err)

              return {
                'statusCode' : 500,
                'headers' : {
                  'context-type' : 'text/json'
                },
                'body' : json.dumps({
                  'error' : '内部エラーが発生しました'
                  })
                }

      Description: !Ref Description
      FunctionName: !Ref FunctionName
      Handler: !Ref Handler 
      MemorySize: !Ref MemorySize
      Runtime: !Ref Runtime
      Timeout: !Ref Timeout
      Role: !GetAtt LambdaRole.Arn
      Tags:
        - Key: "User"
          Value: !Ref TagsName

  LambdaRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub "${FunctionName}-role"
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: !Sub "${FunctionName}-policy"
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                  - "logs:CreateLogGroup"
                Resource: !Sub "arn:${AWS::Partition}:logs:*:*:*"

              - Effect: "Allow"
                Action:
                  - "dynamodb:*" 
                Resource: !Sub "arn:${AWS::Partition}:dynamodb:${AWS::Region}:${AWS::AccountId}:*"

# ------------------------------------------------------------#
# Output Parameters
#------------------------------------------------------------#          
Outputs:
  LambdaArn:
    Value: !GetAtt Lambda.Arn
    Export:
      Name: !Sub "${FunctionName}-arn"
  LambdaName:
    Value: !Ref FunctionName
    Export:
      Name: !Sub "${FunctionName}-name"

2.APIGateway作成:HTMLフォームからの値を受取り、Lambdaを呼び出すため構築

・価格、低レイテンシーの観点より、HTTP API形式でAPIGateWayを構築する。
・Webサイトホスティング画面からAPIGatewayによる呼び出しを行うため、同一オリジンポリシーによる制限がかかる。そのためCORS項目を設定(設定自体は、"*"としている)する。
※参考URL: AWSデベロッパーガイド REST API リソースの CORS を有効にする

AWSTemplateFormatVersion: '2010-09-09'
Description:
  APIGateway Create
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "APIGateway Configuration"
        Parameters:
        - Name
        - StageName
        - TagsName
# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  Name:
    Type: String
    Default: "cfn-apigateway-inamura"
  StageName:
    Type: String
    Default: "$default"
  TagsName:
    Type: String
    Default: "inamura"
# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  APIGateway
# ------------------------------------------------------------#
  HttpApi:
    Type: AWS::ApiGatewayV2::Api
    Properties:
      Name: !Ref Name
      ProtocolType: HTTP
      Tags:
        "User" : !Sub "${TagsName}"
      CorsConfiguration:
        AllowOrigins: 
          - "*"

  HttpApiDefaultStage:
    Type: AWS::ApiGatewayV2::Stage
    Properties:
      ApiId: !Ref HttpApi
      StageName: !Ref StageName
      AutoDeploy: true

  HttpApiIntegration:
    Type: AWS::ApiGatewayV2::Integration
    Properties:
      ApiId: !Ref HttpApi
      IntegrationType: AWS_PROXY
      IntegrationUri: !ImportValue cfn-lmd-writedynamodb-inamura-arn
      PayloadFormatVersion: '2.0'

  HttpApiIntegrationPermission:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !ImportValue cfn-lmd-writedynamodb-inamura-arn
      Principal: apigateway.amazonaws.com
      SourceArn: !Sub "arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${HttpApi}/*/*/${Name}"

  HttpApiHelloRoute:
    Type: AWS::ApiGatewayV2::Route
    Properties:
      ApiId: !Ref HttpApi
      RouteKey: !Sub POST /${Name}
      AuthorizationType: NONE
      Target: !Sub "integrations/${HttpApiIntegration}"

# ------------------------------------------------------------#
# Output Parameters
#------------------------------------------------------------#          
Outputs:
  Endpoint:
    Value: !Sub 'https://${HttpApi}.execute-api.${AWS::Region}.${AWS::URLSuffix}/${Name}'
    Export:
      Name: Endpoint

3.DynamoDB作成:値を DB(テーブル)に登録するため構築

AWSTemplateFormatVersion: '2010-09-09'
Description:
  DynamoDB Create
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "DynamoDB Configuration"
        Parameters:
        - TableName
        - TagsName
# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  TableName:
    Type: String
    Default: "mailaddress"
  TagsName:
    Type: String
    Default: "inamura"

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
  DynamoDB:
    Type: 'AWS::DynamoDB::Table'
    Properties:
      TableName: !Ref TableName
      AttributeDefinitions:
        - AttributeName: email
          AttributeType: S
        - AttributeName: notsend
          AttributeType: N
      KeySchema:
        - AttributeName: email
          KeyType: HASH
      ProvisionedThroughput:
        ReadCapacityUnits: 1
        WriteCapacityUnits: 1
      GlobalSecondaryIndexes:
        - IndexName: notsend-index
          KeySchema:
            - AttributeName: notsend
              KeyType: HASH
          Projection:
            ProjectionType: ALL
          ProvisionedThroughput:
            ReadCapacityUnits: 1
            WriteCapacityUnits: 1
      Tags: 
        - Key: "username"
          Value: !Ref TagsName

Outputs:
  DynamoDBArn:
    Value: !GetAtt DynamoDB.Arn
    Export:
      Name: !Sub "cfn-dynamodb-${TableName}"

4.S3作成:APIGateWayを呼び出すHTMLフォーム保存のため構築

4.1.CFn で S3 構築

index.htmlを配置して、静的ウェブホスティング機能を有効にすることでインターネットに公開をする設定する。
・上記の理由のためブロックパブリックアクセスの4つ全てをFalseにする。
※ただしバケットポリシーを利用して自身の IP からのみアクセス可能とする。
※ IP は 各自確認をする。参照サイト: CMAN

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFormation to create S3 Bucket
# ------------------------------------------------------------#
#  Metadata
# ------------------------------------------------------------#
Metadata:
  "AWS::CloudFormation::Interface":
    ParameterGroups:
      - Label:
          default: "S3 Configuration"
        Parameters:
        - S3BucketName
        - YourIP
        - AccessControl
        - BlockPublicAcls
        - BlockPublicPolicy
        - IgnorePublicAcls
        - RestrictPublicBuckets
        - ExpirationInDays
        - EventBridgeConfiguration
        - Prefix
        - TagsName

# ------------------------------------------------------------#
#  InputParameters
# ------------------------------------------------------------#
Parameters:
  S3BucketName:
    Type: String
    Default: "cfn-s3-2023016-inamura"
    Description: Type of this BacketName.
  YourIP:
    Type: String 
    Default: "【自身のIPアドレスを入力】"
    Description: YourIP for BucketPolicy writing
  VersioningConfiguration:
    Type: String
    Default: "Enabled"
    Description: VersioningConfiguration.
  AccessControl:
    Type: String
    Description: AccessControl.
    Default: "PublicRead"
    AllowedValues: [ "Private", "PublicRead", "PublicReadWrite", "AuthenticatedRead", "LogDeliveryWrite", "BucketOwnerRead", "BucketOwnerFullControl", "AwsExecRead" ]
  BlockPublicAcls: 
    Type: String
    Description: BlockPublicAcls.
    Default: "False"
    AllowedValues: [ "True", "False" ]
  BlockPublicPolicy:
    Type: String
    Description: BlockPublicPolicy.
    Default: "False"
    AllowedValues: [ "True", "False" ]
  IgnorePublicAcls:
    Type: String
    Description: IgnorePublicAcls.
    Default: "False"
    AllowedValues: [ "True", "False" ]
  RestrictPublicBuckets:
    Type: String
    Description: RestrictPublicBuckets.
    Default: "False"
    AllowedValues: [ "True", "False" ]
  ExpirationInDays:
    Type: String
    Description: Lifecycle Days.
    Default: "7"
  TagsName:
    Type: String
    Description: UserName
    Default: "inamura"

# ------------------------------------------------------------#
#  Resources
# ------------------------------------------------------------#
Resources:
# ------------------------------------------------------------#
#  S3
# ------------------------------------------------------------#
  S3Bucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref S3BucketName      
      VersioningConfiguration:
        Status: !Ref VersioningConfiguration
      AccessControl: !Ref AccessControl
      PublicAccessBlockConfiguration:
        BlockPublicAcls: !Ref BlockPublicAcls
        BlockPublicPolicy: !Ref BlockPublicPolicy
        IgnorePublicAcls: !Ref IgnorePublicAcls
        RestrictPublicBuckets: !Ref RestrictPublicBuckets
      LifecycleConfiguration:
        Rules:
          - Id: LifeCycleRule
            Status: Enabled
            ExpirationInDays: !Ref ExpirationInDays
      WebsiteConfiguration:
        IndexDocument: !index.html
      Tags:
        - Key: "User"
          Value: !Ref TagsName

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Statement:
        - Action:
          - s3:GetObject
          - s3:GetObjectVersion
          Effect: Allow
          Resource: !Sub "arn:aws:s3:::${S3BucketName}/*"
          Principal: "*"
          Condition:
            IpAddress: 
              aws:SourceIp: !Ref YourIP

# ------------------------------------------------------------#
#  Outputs
# ------------------------------------------------------------#
Outputs:
  S3BucketName:
    Value: !Ref S3Bucket
    Export:
      Name: cfn-s3-BucketName
  S3WebsiteURL:
    Value: !Sub "http://${S3BucketName}.s3-website-${AWS::Region}.amazonaws.com"
    Export:
      Name: cfn-s3-WebsiteURL

4.2. S3 に保存する index.html を作成

APIGatewayusernameemailnotsendsend、の値を送るフロントの画面を作成する。
※値に関しては、前回ブログ: Lambda + DynamoDB を利用したサーバレス構築ハンズオン で作成した DynamoDB に合わせています。

・【 APIGatewayエンドポイント 】に関しては、手順1で構築したLambdaのページより確認する。
下記画面の赤枠部分をコピペして貼り付けてからS3に保存する

<!doctype html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scan=1, shrink-to-fit=no">
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <script>
      function onSubmit() { 
        var data = {
          'username' : $('#username').val(),
          'email' : $('#email').val(),
          'notsend' : $('#notsend').val(),
          'send' : $('#send').val()
        };

        $.ajax({
          'type' : 'POST',
          'url' : 'https://【 APIGatewayエンドポイント 】.execute-api.ap-northeast-1.amazonaws.com/writedynamodb',
          'contentType' : 'text/plain',
          'data' : JSON.stringify(data)
        }).done(function (data, textStatus, jqXHR) {
        // 成功
        alert('送信完了しました');
        $('#username').val(''); $('#email').val(''); $('#notsend').val(''); $('#send').val('')
      }).fail(function (jqXHR, textStatus, errorThrown) {
        var err = [];
        try {
          err = $.parseJSON(jqXHR.responseText);
        } catch (e) {
        }
        alert('エラーが発生しました' + err['error']);
        });
        }
    </script>
  </head>

  <body>
    <div class="container">
      <h1>ユーザ登録</h1>
      <div class="form-group">
        <label for="username">氏名</label>
        <input type="text" class="form-control" id="username">
      </div>
      <div class="form-group">
        <label for="email">メールアドレス</label>
        <input type="text" class="form-control" id="email">
        <input type="hidden"  class="form-control" id="notsend" value="0">
        <input type="hidden"  class="form-control" id="send" value="1">
      </div>
      <button id="submit" class="btn btn-primary" onclick="onSubmit();return false;">送信</button>
  </body>
</html>

4.3. S3 に index.html を配置する

4.3.1.S3の構築したバケットにindex.htmlを配置する

4.3.2. 構築したバケットの> に index.html を配置する

タブ:プロパティ > 静的ウェブサイトホスティング の URL を押下する

4.3.3 index.htmlのページに遷移する

挙動の確認

① index.html に名前・メールアドレスを登録・送信ボタンを押下する

※バリデーションなど設定していないので、なんでも登録できるのが実態です。

②送信ボタン押下後、『送信完了しました』が表示される

DynamoDBに登録されていることを確認する

DynamoDB > 左ペイン:テーブル の 項目を探索 を押下 > テーブル名(画面ではmailaddressしかない)を選択


さいごに

前回に引き続き盛りだくさんの内容になりましたが、DynamoDBに名前やアドレスを CLIからではなくて、実際のフロント画面から送ったりして挙動を確認出来たかと思います。
前回の構築と合わせれば、画面で登録されたユーザに対してメールを送るなどの挙動も可能となりました。

構築したことにより、S3でウェブサイトホスティングから、異なるサーバに値するAPIGatewayに値を送信するためのCORSの理解が捗りました。
それとDynamoDBをGSIの構築をしていたため、単に値を送信するのではなくて、型の指定をしておかないと弾かれることなど、時間をかけて理解を進めることができました。


参考文献

AWS Lambda実践ガイド 第2版 (impress top gear) 著:大澤文孝

Last modified: 2023-01-22

Author