はじめに
普段DynamoDBをあまり使用しないので、CFnの練習がてら構築していきたいと思います。
前回のブログでは、CLIからDynamoDBに値を渡していましたが、いっそのことフロント画面から送り保存すれば、より学習が捗るかと思いまして着手してみました。
といってもフロント画面などさっぱりなので、前回に引き続き学習教本として、AWS Lambda実践ガイド 第2版 著:大澤文孝
を利用しています。
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 を作成
・APIGateway
にusername
、email
、notsend
、send
、の値を送るフロントの画面を作成する。
※値に関しては、前回ブログ: 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の構築をしていたため、単に値を送信するのではなくて、型の指定をしておかないと弾かれることなど、時間をかけて理解を進めることができました。