こんにちは。
最近、DynamoDB について触る機会があったので、少し勉強してみようかな、と。
調べてみると ローカルに DynamoDB を立てることができる dynamodb-local というものがあるそうです。
SAM と dynamodb-local を使ってローカルにサーバレス開発環境を作り、簡単な掲示板を作成してみようと思います。
構成
JavaScript:クライアント
SAM ( ApiGateWay / Lambda ) : バックエンド API
dynamodb-local:データストア
サーバ IP:192.168.33.55
よくあるタイプのやつですね。
SAM ( ApiGateWay / Lambda ) でデータ取得 / データ登録 の REST API を作って、 JavaScript で操作、データは dynamodb-local に保存します。
環境構築
dynamodb-local は Docker のイメージが公開されています。
CentOS7 に SAM と dynamodb-local をセットアップする Ansible の role を こちら に用意しました。
環境構築後は、事前に dynamodb-local にテーブルを作成しておきます。
# テーブル作成
aws dynamodb create-table --table-name 'bbs' \
--attribute-definitions '[{"AttributeName":"postId","AttributeType": "S"}]' \
--key-schema '[{"AttributeName":"postId","KeyType": "HASH"}]' \
--provisioned-throughput '{"ReadCapacityUnits": 5,"WriteCapacityUnits": 5}' \
--endpoint-url http://localhost:8000
バックエンド API
データ取得 / データ登録 の API を Python で作成します。
ディレクトリ構成は下記の通りです。
.
├── bbs
│ ├── create
│ │ └── app.py
│ └── read
│ └── app.py
├── template.yaml
└── vars.json
・データ取得 API ( ./bbs/read/app.py )
import json
import boto3
import time
import logging
import os
from botocore.exceptions import ClientError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info("Event: " + str(event))
responseHeaders = {
"Access-Control-Allow-Methods": "OPTIONS,GET",
"Access-Control-Allow-Headers" : "*",
"Access-Control-Allow-Origin": "*"
}
try:
table = _get_database().Table('bbs')
res = table.scan()
except ClientError as e:
logger.error("Error: %s", e)
return {
"headers": responseHeaders,
"statusCode": 200,
"body": json.dumps(res['Items'], default=decimal_default_proc),
}
def decimal_default_proc(obj):
from decimal import Decimal
if isinstance(obj, Decimal):
return float(obj)
raise TypeError
def _get_database():
if (os.environ["DYNAMO_ENDPOINT"] == ""):
endpoint = boto3.resource('dynamodb')
else:
endpoint = boto3.resource('dynamodb', endpoint_url=os.environ["DYNAMO_ENDPOINT"])
return endpoint
・データ登録 API ( ./bbs/create/app.py )
import json
import boto3
import time
import datetime
import logging
import uuid
import os
from boto3.dynamodb.conditions import Key, Attr
from botocore.exceptions import ClientError
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
logger.info("Event: " + str(event))
responseHeaders = {
"Access-Control-Allow-Methods": "OPTIONS,GET,POST",
"Access-Control-Allow-Headers" : "*",
"Access-Control-Allow-Origin": "*"
}
req = json.loads(event['body'])
item = {
'postId': str(uuid.uuid4()),
'time': str(datetime.datetime.now()),
'owner': req['owner'],
'note': req['note'],
'enable': bool('True')
}
logger.info("Item: " + str(item))
try:
table = _get_database().Table('bbs')
res = table.put_item(Item=item)
logger.info("Respons: " + str(res))
except ClientError as e:
logger.error("Error: %s", e)
return {
"headers": responseHeaders,
"statusCode": 200,
"body": item,
}
def _get_database():
if (os.environ["DYNAMO_ENDPOINT"] == ""):
endpoint = boto3.resource('dynamodb')
else:
endpoint = boto3.resource('dynamodb', endpoint_url=os.environ["DYNAMO_ENDPOINT"])
return endpoint
・環境変数定義ファイル : DynamoDB への接続先エンドポイント ( ./vars.json )
{
"Parameters": {
"DYNAMO_ENDPOINT": "http://192.168.33.55:8000"
}
}
・SAM の template ファイル : API の仕様やその他諸々 ( ./template.yaml )
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: BBS
Globals:
Function:
Timeout: 3
Environment:
Variables:
DYNAMO_ENDPOINT: DYNAMO_ENDPOINT
Api:
Cors:
AllowMethods: "'OPTIONS,GET,POST'"
AllowHeaders: "'*'"
AllowOrigin: "'*'"
Resources:
ReadBBSFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: bbs/read/
Handler: app.lambda_handler
Runtime: python3.9
Events:
ReadBBS:
Type: Api
Properties:
Path: /bbs/
Method: get
CreateBBSFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: bbs/create/
Handler: app.lambda_handler
Runtime: python3.9
Events:
CreateBBS:
Type: Api
Properties:
Path: /bbs/
Method: post
API の起動
コードをビルドして API を起動します。
※ デフォルトは 3000 ポートで立ち上がります。
sam build
sam local start-api --env-vars vars.json --host 0.0.0.0
curl で API を叩いてエラー無くレスポンスが返ってくれば OK です。
※ 初回リクエスト時にイメージがビルドされるようで、その際レスポンスが少々遅延するようです。
$ curl http://192.168.33.55:3000/bbs
[]
クライアント
Ajax でデータ取得 API を叩いて投稿情報を取得し、一覧を表示します。
併せてデータ登録 API に投稿を POST して登録を行います。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>bbs</title>
</head>
<body>
<dl id="wrap">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script type="text/javascript">
const url = "http://192.168.33.55:3000/bbs";
$(function(){
$.getJSON(url, function(note_list){
note_list.sort(function(a, b){
if (a.time < b.time){
return -1;
}else{
return 1;
}
});
for(var i in note_list){
var h = '<dt>'
+ note_list[i].time
+ " "
+ note_list[i].owner
+ '</dt>'
+ '<dd>'
+ note_list[i].note
+ '</dd>';
$("dl#wrap").append(h);
}
});
});
</script>
</dl>
<p>投稿者: <input type="text" id="owner" size="30"></p>
<p>NOTE: <input type="text" id="note" size="30"></p>
<p><button id="button" type="button">投稿</button></p>
<script type="text/javascript">
$(function(){
$("#response").html("Response Values");
$("#button").click( function(){
var url = "http://192.168.33.55:3000/bbs";
var JSONdata = {
"owner": String($("#owner").val()) ,
"note": String($("#note").val())
};
$.ajax({
type : 'post',
url : url,
data : JSON.stringify(JSONdata),
contentType: 'application/json',
dataType : 'json',
scriptCharset: 'utf-8',
success : function(time){
window.location.reload();
},
error : function(time){
window.location.reload();
}
});
})
})
</script>
</body>
</html>