こんにちは。
最近、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>