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

## はじめに
普段DynamoDBをあまり使用しないので、CFnの練習がてら構築していきたいと思います。
[前回のブログ](https://cloud5.jp/lambda-dynamodb-serverless/)では、CLIからDynamoDBに値を渡していましたが、いっそのことフロント画面から送り保存すれば、より学習が捗るかと思いまして着手してみました。

といってもフロント画面などさっぱりなので、前回に引き続き学習教本として、[AWS Lambda実践ガイド 第2版  著:大澤文孝](https://www.amazon.co.jp/AWS-Lambda%E5%AE%9F%E8%B7%B5%E3%82%AC%E3%82%A4%E3%83%89-%E7%AC%AC2%E7%89%88-impress-gear/dp/4295013307/ref=asc_df_4295013307/?tag=jpgo-22&linkCode=df0&hvadid=553954433678&hvpos=&hvnetw=g&hvrand=2164190274868650654&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009299&hvtargid=pla-1599229625688&psc=1&th=1&psc=1)
を利用しています。
GUI及び、SAMによる構築はありましたが、CFnでの構築はなかったので、今回も書籍のエッセンスを使いつつ、自分なりに少しアーキテクトを変更して構築をしてみました。

------------

## 構成図
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-10.50.20-640x322.png)

------------

## ハンズオン
### 構築のながれ
#### 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`へ型の指定を行っている。

```yaml
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"
```
<br>

### 2.APIGateway作成：HTMLフォームからの値を受取り、Lambdaを呼び出すため構築
・価格、低レイテンシーの観点より、`HTTP API`形式で`APIGateWay`を構築する。
・Webサイトホスティング画面からAPIGatewayによる呼び出しを行うため、同一オリジンポリシーによる制限がかかる。そのため`CORS`項目を設定(設定自体は、`"*"`としている)する。
※参考URL： [AWSデベロッパーガイド REST API リソースの CORS を有効にする](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/how-to-cors.html)

```yaml
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
```
<br>

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

```yaml
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}"
```
<br>

### 4.S3作成：APIGateWayを呼び出すHTMLフォーム保存のため構築
#### 4.1.CFn で S3 構築
・`index.html`を配置して、静的ウェブホスティング機能を有効にすることでインターネットに公開をする設定する。
・上記の理由のため`ブロックパブリックアクセス`の4つ全てを`False`にする。
※ただし`バケットポリシー`を利用して自身の IP からのみアクセス可能とする。
※ IP は 各自確認をする。参照サイト： [CMAN](https://www.cman.jp/network/support/go_access.cgi)
```yaml
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
```
<br>

#### 4.2. S3 に保存する index.html を作成
・`APIGateway`に`username`、`email`、`notsend`、`send`、の値を送るフロントの画面を作成する。
※値に関しては、[前回ブログ: Lambda + DynamoDB を利用したサーバレス構築ハンズオン](https://cloud5.jp/lambda-dynamodb-serverless/) で作成した `DynamoDB` に合わせています。

・【 APIGatewayエンドポイント 】に関しては、手順1で構築した`Lambda`のページより確認する。
下記画面の赤枠部分をコピペして貼り付けてからS3に保存する
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-10.08.03-640x318.png)

<br>

```html
<!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>
```
<br>

#### 4.3. S3 に index.html を配置する
##### 4.3.1.S3の構築したバケットに`index.html`を配置する
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-9.50.16-640x129.png)


##### 4.3.2.  構築したバケットの>  に index.html を配置する
タブ：プロパティ > 静的ウェブサイトホスティング　の URL を押下する
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-9.56.49-640x227.png)

#### 4.3.3 `index.html`のページに遷移する
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-10.00.05-640x149.png)

<br />

### 挙動の確認
#### ① index.html に名前・メールアドレスを登録・送信ボタンを押下する
※バリデーションなど設定していないので、なんでも登録できるのが実態です。
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-10.19.14.png)

#### ②送信ボタン押下後、『送信完了しました』が表示される
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-10.14.52-1-640x263.png)

#### ③`DynamoDB`に登録されていることを確認する
DynamoDB > 左ペイン：テーブル の 項目を探索 を押下 >　テーブル名（画面では`mailaddress`しかない）を選択
![](https://cloud5.jp/wp-content/uploads/2023/01/スクリーンショット-2023-01-22-10.15.58-640x208.png)

<br />

------------

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

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

------------

## 参考文献
[AWS Lambda実践ガイド 第2版 著:大澤文孝](https://www.amazon.co.jp/AWS-Lambda%E5%AE%9F%E8%B7%B5%E3%82%AC%E3%82%A4%E3%83%89-%E7%AC%AC2%E7%89%88-impress-gear/dp/4295013307/ref=asc_df_4295013307/?tag=jpgo-22&linkCode=df0&hvadid=553954433678&hvpos=&hvnetw=g&hvrand=2164190274868650654&hvpone=&hvptwo=&hvqmt=&hvdev=c&hvdvcmdl=&hvlocint=&hvlocphy=1009299&hvtargid=pla-1599229625688&psc=1&th=1&psc=1)