フォーム読み込み中
2023年2月24日掲載
Amazon Web Service DynamoDBは、大量の構造化データ及び半構造化データを保存したり処理する、フルマネージド型NoSQLデータベースサービスです。DynamoDBを使用すると、小規模から開始してグローバルまで拡張できる最新のサーバーレスアプリケーションを構築して数ペタバイトのデータや 1 秒あたり数千万の読み込みおよび書き込みリクエストをサポートすることができます。
(AWS公式サイトより引用)
本記事では、AWS上のDynamoDBとPython or Node.jsを使って、簡単なRESTful APIサービスを構築する方法をご紹介します。
DynamoDBを使った、RESTful APIサービス作成方法としての流れ及び全体像は次の図の通りになります。
DynamoDBを利用するにあたって、DynamoDBのテーブルを作成します。テーブル操作はAWSコンソールからウィザードで簡単に作成できます。チャットサービス(IM Service)を想定し、このテーブルを使用してメッセージを保存します。
チャットグループ名がパーティションキー、生成されたメッセージIDがソートキーになります。キーの他に、メッセージの内容、送信者、送信時刻などの属性があります。
「Create table」ボタンをクリックすると、テーブルの作成プロセスに入ります。
テーブル名とキーの設定値を入力します。
今回はテストとして、デフォルトのテーブル設定を使用します。「Customize settings」で自由に更新します。
成功のメッセージとともに、まもなくターゲット・テーブルが利用可能になります。
そして、対象テーブルの名前をクリックすると、詳細を確認することができます。
DynamoDBのテーブルは、コンソールから複数の操作をサポートしています。ドロップダウンリストや「Explorer table items」ボタンをクリックして項目ビューページに入ると、それらの操作を実行することができます。
以下の手順で項目を作成し、データをセットします。
ドロップダウンリストの中から「Create item」ボタンをクリックします。
キーの値を入力します。
「Add new attribute」をクリックすると、データ型のドロップダウンリストが表示されます。
属性のデータ型を選択します。例えば、メッセージの内容の属性には `String` を選択します。
属性の名前と値を一つずつ入力していきます。
ページが項目ビューページにジャンプし、新しく作成されたデータ項目が表示されます。
上記のようにフォームで新しいデータ項目を作成する以外に、JSONビューで作成することもできます。「Create item」ページで「JSON view」ボタンをクリックすると、JSONモードに切り替わります。エディタでデータ項目を更新して、JSONから作成することができます。
なお、JSONビューは一般的なJSON形式とDynamoのJSON形式の両方をサポートしています。切り替えで変更することができます。
JSONデータを準備して「Create item」ボタンをクリックすると、新しいデータ項目が作成されます。
特定のフィルターでテーブルをスキャンすることができます。
項目ビューページで 「Scan」 ボタンをクリックします。
「Filters」をクリックすると、フィルタの設定が表示されます。
新しいフィルタを追加するには、「Add Filter」ボタンをクリックします。
属性と条件を指定してフィルタを設定します。
これにより、フィルターに基づいたテーブルの中で関連するデータが見つかります。
フィルタで正しいデータ型を設定したことを確認します。そうでない場合は、データが得られません。
クエリモードは項目ビューページでも利用できます。
項目ビューページで 「Query」ボタンをクリックします。
パーティションキーに値を設定します。
ソートキーの値と条件を設定します。
「Filters」をクリックすると、フィルターの設定が表示されます。
「Add Filter」ボタンをクリックして、新しいフィルタを追加します。
属性と条件を指定してフィルタを設定します。
すると、そこに関連するデータ項目が表示されます。
「Download results to csv」ボタンをクリックすると、データ項目をダウンロードできます。ダウンロードされたファイルには、スキャンやクエリの結果が属性名付きで含まれています。
特定の1つのデータをピックアップして、属性を更新します。
選択したデータ項目のチェックボックスにチェックを入れ、「Edit item」ボタンをクリックします。
既存の属性を更新するか、更新として新しい属性を追加します。
完了すると、結果リストに更新されたデータ項目が表示されます。
「Duplicate item」ボタンをクリックして、データ項目をコピーします。
選択した項目の現在のデータ値を「Copy item」ページに表示することで、ページ内の既存の1つのデータ項目から新しいデータ項目を作成することができます。
ページ内には、削除機能も準備されています。関連するデータ項目をピックアップし、削除操作を確定すると、テーブルから対象のデータ項目が削除されます。
コンソールからDynamoDBを一通り操作したので、今度はAWS CLIを使って操作をします。この操作にあたり、AWS CLI のインストールと設定が必要になります。AWS公式サイトのヘルプドキュメントに従って、AWS CLIをインストールし、設定します。
<参考>
Installing or updating the latest version of the AWS CLI
https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html
Configuration basics
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html
AWS CLIの導入が完了したら、データ項目を操作してみます。AWS CLIには、DynamoDB用のコマンドが多数準備されています。
このページや、上記のヘルプコマンドで確認することができます。一般的な機能のコマンドをいくつか紹介します。
<参考>
DynamoDB CLI コマンドリスト
https://docs.aws.amazon.com/cli/latest/reference/dynamodb/index.html
テーブルに新しいデータ項目を挿入します。
aws dynamodb put-item --table-name ohara-bob-dynamo --item '{ "group": {"S": "CLI"},"id": {"N": 20220920001},"sender": {"S": "Bob"},"message": {"S": "From CLI"},"timetag": {"S": "202209201400123"} }' --return-consumed-capacity TOTAL
対象テーブルを無条件でスキャンします。1回のスキャン操作で、設定した最大項目数(Limitパラメータを使用した場合)または最大1MBのデータを読み取り、FilterExpressionを使用して結果に任意のフィルタリングを適用します。
aws dynamodb scan --table-name ohara-bob-dynamo
特定のデータ項目を確認します。
aws dynamodb get-item --table-name ohara-bob-dynamo --key "{ \"group\": {\"S\": \"CLI\"},\"id\": {\"N\": \"20220920001\"} }"
テーブルから項目を削除し、get-itemとスキャン操作でデータを確認します。
aws dynamodb delete-item --table-name ohara-bob-dynamo --key "{ \"group\": {\"S\": \"CLI\"},\"id\": {\"N\": \"20220920001\"} }"
AWS CLIによるDynamoDB操作は概ね把握できたと思うので、今度はNode.jsを使って、ローカル向けにDynamoDBによるRESTful APIサービスを作ります。この構築にはDynamoDBのNode.js SDKが必要なので、Node.js SDKを事前に導入する必要があります。AWSでは、Node.js SDK with DynamoDBの紹介ページとサンプルコードが公開されています。上記テストとして利用したテーブルでそのまま作業を行います。
<参考>
データや項目を DynamoDB テーブルに書き込むhttps://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.WriteItem.html
DynamoDB code examples for the SDK for JavaScript (v3)
https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/javascriptv3/example_code/dynamodb#code-examples
RESTful APIを構築するために、実行環境としてプロジェクトフォルダの準備に移ります。node_aws_dynamoという新規プロジェクトフォルダを作成し、その中にnpmコマンドで依存パッケージをインストールします。
DynamoDBとの連携には@aws-sdk/client-dynamodbと@aws-sdk/lib-dynamodbを、ローカルサーバの起動にはexpress、メッセージ属性のタイムスタンプの生成にはmomentを使用します。
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb express moment --save
node_aws_dynamoというプロジェクトフォルダの準備ができたら、ローカルRESTful APIサーバーの構築にあたり、2つのスクリプト server.js、message.jsを準備します。 server.js、message.jsのコードは以下の通りです。
server.jsは、expressサーバの設定とルートを定義しています。
"use strict";
const express = require('express');
const app = express();
const message = require('./message');
app.use(express.json());
app.get('/', (req, res) => {
res.end('Welcome to DynamoDB RESTful API demo!');
});
app.get('/api/msg/:group', async (req, res) => {
var group = req.params.group;
var results = await message.listMessages(group);
res.json(results).end();
});
app.get('/api/msg/:group/:id', async (req, res) => {
var group = req.params.group;
var id = parseInt(req.params.id);
var results = await message.getMessageByGroupAndId(group, id);
res.json(results).end();
});
app.post('/api/msg/:group', async (req, res) => {
var group = req.params.group;
var id = req.body.id;
var sender = req.body.sender;
var msg = req.body.message;
var results = await message.saveMessage(group, id, sender, msg);
res.json(results).end();
});
app.delete('/api/msg/:group/:id', async (req, res) => {
var group = req.params.group;
var id = parseInt(req.params.id);
var results = await message.deleteMessage(group, id);
res.json(results).end();
});
const port = process.env.PORT || 5000;
app.listen(port, () => console.log(`Listening on port ${port}`));
message.jsでは、DynamoDBのデータや項目を操作するための関数を定義しています。各自の作業環境でスクリプトを実行する前に、テーブル名を自分のものに更新します。
"use strict";
const moment = require('moment');
const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
const { PutCommand, GetCommand, DeleteCommand, ScanCommand } = require("@aws-sdk/lib-dynamodb");
const client = new DynamoDBClient({ region: "ap-northeast-1" });
const table = "ohara-bob-dynamo";
/**
* List all the messages in the database
*/
async function listMessages(group) {
var params = {
FilterExpression: "#group = :g",
ExpressionAttributeValues: {
":g": group,
},
ExpressionAttributeNames: { "#group": "group" },
TableName: table,
};
try {
const data = await client.send(new ScanCommand(params));
if (data.Items.length == 0) {
return { "Status": "Failed", "results": "No target messages.", "data": data.Items };
} else {
return { "Status": "Success", "results": "Get target messages successfully.", "data": data.Items };
}
} catch (err) {
console.log("Error", err);
return { "Status": "Failed", "results": "Failed to get target message.", "data": null, "error": err };
}
}
/**
* Get target message with specific message group and id
*/
async function getMessageByGroupAndId(group, id) {
var params = {
TableName: table,
Key: {
group: group,
id: id,
},
};
try {
const data = await client.send(new GetCommand(params));
if (undefined == data.Item) {
return { "Status": "Failed", "results": "No target message.", "data": null };
} else {
return { "Status": "Success", "results": "Get target messages successfully.", "data": data.Item };
}
} catch (err) {
console.log("Error", err);
return { "Status": "Failed", "results": "Failed to get target message.", "data": null, "error": err };
}
}
/**
* Save message with passed data
*/
async function saveMessage(group, id, sender, message) {
var params = {
TableName: table,
Item: {
group: group,
id: id,
sender: sender,
message: message,
timetag: moment().format('YYYY-MM-DD HH:mm:ss')
},
};
try {
const data = await client.send(new PutCommand(params));
return { "Status": "Success", "results": "Save target message successfully.", "data": data };
} catch (err) {
console.log("Error", err.stack);
return { "Status": "Failed", "results": "Failed to save target message.", "error": err };
}
}
/**
* Delete target message with specific message id and group
*/
async function deleteMessage(group, id) {
var params = {
TableName: table,
Key: {
group: group,
id: id,
},
};
try {
const data = await client.send(new DeleteCommand(params));
return { "Status": "Success", "results": "Delete target message successfully.", "data": data };
} catch (err) {
console.log("Error", err.stack);
return { "Status": "Failed", "results": "Failed to delete target message.", "error": err };
}
}
module.exports = { listMessages, getMessageByGroupAndId, saveMessage, deleteMessage }
最後に、以下のようなプロジェクト構成で、ローカルの RESTful API サーバーが準備できました。
.
├── node_modules
├── message.js
├── package.json
└── server.js
ここまで、コードら実行ファイルの準備ができたら、サーバーを起動し、RESTful APIをテストします。npmコマンドでサーバを起動するには、起動設定にあるpackage.jsonを更新する必要があります。または、プロジェクトフォルダのルートにある `node server.js` を直接実行することもできます。
……
"scripts": {
"start": "node server.js"
},
……
`npm start`コマンドを実行して、ポート5000でexpressサーバーを起動します。
以下の手順で、curlコマンドに基づいたリクエストを送信します。冒頭のメッセージを確認します。
curl -X GET http://localhost:5000/
テーブルに一時的なメッセージを1つ保存します。
curl -H "Content-Type: application/json" -X POST -d "{\"id\": 20220920001,\"sender\":\"bob\",\"message\":\"Testing message from Node.js .\"}" http://localhost:5000/api/msg/node
作成されたメッセージ情報をテーブルから確認します。
curl -X GET http://localhost:5000/api/msg/node/20220920001
テーブル内にデータが見つからない場合、APIは通知メッセージを返します。
同じチャットグループの下にあるテーブルに別のメッセージを挿入します。その後、テーブルからターゲットチャットグループのすべてのメッセージをスキャンします。
curl -H "Content-Type: application/json" -X POST -d "{\"id\": 20220920002,\"sender\":\"bob\",\"message\":\"2nd message from Node.js .\"}" http://localhost:5000/api/msg/node
curl -X GET http://localhost:5000/api/msg/node
チャットグループ名とメッセージIDを指定して、テーブルから特定のメッセージを削除します。操作後、再度テーブルからメッセージを検索します。
curl -X DELETE http://localhost:5000/api/msg/node/20220920001
curl -X GET http://localhost:5000/api/msg/node/20220920001
Node.js によるRESTful APIは構築できたので、今度はPythonとflaskパッケージを使ってRESTful APIを作っde.js SDKと同様にDynamoDBのPython SDKが必要なので、DynamoDB Python SDKてみます。この構築にはNoを事前に導入する必要があります。AWSではNode.jsと同じく、Python SDK with DynamoDBの紹介ページやサンプルコードも準備されています。関数の使い方がよくわからない場合は、AWS SDK for Python (Boto3)のドキュメントを参考にするとよいでしょう。
<参考>
AWS SDK を使用して、DynamoDB テーブルを作成するhttps://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/GettingStarted.CreateTable.html
DynamoDB examples for the SDK for Python
https://github.com/awsdocs/aws-doc-sdk-examples/tree/main/python/example_code/dynamodb
boto3 doc DynamoDB
https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html
python_aws_dynamoという仮想環境によるプロジェクトフォルダを準備し、以下のコマンドで仮想環境を起動します。
py -m venv .venv
.venv\scripts\activate
必要な依存パッケージをインストールし、その情報をrequirements.txtに保存します。boto3はPythonのAWS SDKで、flask-restfulはRESTful APIサーバーを構築するために使用されます。
pip install boto3 flask-restful
pip freeze > requirements.txt
Pythonをベースにしたコード構成は、Node.jsのものと同じです。
server.pyでは、サーバーの設定とルートを定義しています。
from flask import Flask, jsonify
from flask_restful import Api, Resource, reqparse
from dynamo import DynamoManager
class Message(Resource):
def __init__(self):
self.manager = DynamoManager()
super().__init__()
def get(self, group, id):
return jsonify(self.manager.read_message_by_id(group, int(id)))
def delete(self, group, id):
return jsonify(self.manager.delete_message_by_id(group, int(id)))
class Messages(Resource):
def __init__(self):
self.manager = DynamoManager()
super().__init__()
def get(self, group):
return jsonify(self.manager.read_all_messages(group))
def post(self, group):
args = reqparse.RequestParser() \
.add_argument('id', type=str, location='json', required=True, help="Empty id") \
.add_argument('sender', type=str, location='json', required=True, help="Empty sender") \
.add_argument('message', type=str, location='json', required=True, help="Empty message") \
.parse_args()
return jsonify(self.manager.save_message(group, int(args['id']), args['sender'], args['message']))
app = Flask(__name__)
api = Api(app, default_mediatype="application/json")
api.add_resource(Message, '/api/msg/<group>/<id>')
api.add_resource(Messages, '/api/msg/<group>')
app.run(host='0.0.0.0', port=5001, use_reloader=True)
dynamo.pyには、DynamoDBサービスと通信するための関数が定義されています。
import boto3
from botocore.exceptions import ClientError
import datetime
class DynamoManager:
def __init__(self):
self.dyn_resource = boto3.resource('dynamodb')
self.table = self.dyn_resource.Table('ohara-bob-dynamo')
def save_message(self, group, id, sender, message):
try:
self.table.put_item(
Item = {
'group': group,
'id': id,
'sender': sender,
'message': message,
'timetag': datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
)
return {"status": "Success", "results": "Save the target message successfully."}
except ClientError as err:
errCode = err.response['Error']['Code']
errMessage = err.response['Error']['Message']
return {"status": "Failed", "results": "Failed to save target message.", "errCode": errCode, "errMsg": errMessage}
def read_all_messages(self, group):
messages = []
scan_kwargs = {
'FilterExpression': "#group = :g",
'ExpressionAttributeValues': {":g": group,},
'ExpressionAttributeNames': {"#group": "group"}
}
try:
done = False
start_key = None
while not done:
if start_key:
scan_kwargs['ExclusiveStartKey'] = start_key
response = self.table.scan(**scan_kwargs)
messages.extend(response.get('Items', []))
start_key = response.get('LastEvaluatedKey', None)
done = start_key is None
if len(messages) == 0:
return {"status": "Failed", "errMessage": "No target messages."}
else:
return {"status": "Success", "results": "Get target messages successfully.", "data": messages}
except ClientError as err:
errCode = err.response['Error']['Code']
errMessage = err.response['Error']['Message']
return {"status": "Failed", "results": "Failed to fetch target messages.", "errCode": errCode, "errMsg": errMessage}
def read_message_by_id(self, group, id):
try:
result = self.table.get_item(Key={'group': group, 'id': id})
if 'Item' in result:
return {"status": "Success", "results": "Get the target message successfully.", "data": result['Item']}
else:
return {"status": "Failed", "errMessage": "No target data row."}
except ClientError as err:
errCode = err.response['Error']['Code']
errMessage = err.response['Error']['Message']
return {"status": "Failed", "results": "Failed to get target message.", "errCode": errCode, "errMsg": errMessage}
def delete_message_by_id(self, group, id):
try:
self.table.delete_item(Key={'group': group, 'id': id})
return {"status": "Success", "results": "Delete the target message successfully."}
except ClientError as err:
errCode = err.response['Error']['Code']
errMessage = err.response['Error']['Message']
return {"status": "Failed", "results": "Failed to delete target message.", "errCode": errCode, "errMsg": errMessage}
最終的なプロジェクトの構成は、次の通りになります。
.
├── .venv
├── dynamo.py
├── requirements.txt
└── server.py
ここまで、コードら実行ファイルの準備ができたら、サーバーを起動し、RESTful APIをテストします。`python server.py`コマンドを実行し、flask サーバを起動します。
curl コマンドで API 経由でテーブルに新しいメッセージを挿入できるかテストします。
curl -H "Content-Type: application/json" -X POST -d "{\"id\": 20220921001,\"sender\":\"bob\",\"message\":\"Testing message from Python .\"}" http://localhost:5001/api/msg/python
特定のチャットグループ情報とメッセージIDでメッセージ情報を確認します。
curl -X GET http://localhost:5001/api/msg/python/20220921001
同じチャットグループの下のテーブルに別の新しいメッセージを挿入し、それらのメッセージをテーブルでスキャンします。
curl -H "Content-Type: application/json" -X POST -d "{\"id\": 20220921002,\"sender\":\"bob\",\"message\":\"2nd message from Python .\"}" http://localhost:5001/api/msg/python
curl -X GET http://localhost:5001/api/msg/python
特定のチャットグループ情報とメッセージIDを持つターゲットメッセージを削除します。その後、同じパラメータで再度テーブルからメッセージを検索します。期待通り、何もデータがない通知メッセージが表示されます。
curl -X DELETE http://localhost:5001/api/msg/python/20220921001
curl -X GET http://localhost:5001/api/msg/python/20220921001
DynamoDBのJSONではなく、一般的なJSONを使用しているため、渡されるパラメータは特定のデータ型に従わなければなりません。そうでない場合はエラーメッセージが表示されます。
UnrecognizedClientException:
エラーメッセージ:
An error occurred (UnrecognizedClientException) when calling the ListFunctions operation: The security token included in the request is invalid.
アクセスキーの設定が間違っていることが原因ですので、認証情報の設定を確認します。
Unknown options:
エラーメッセージ:
usage: aws [options] <command> <subcommand> [<subcommand> ...] [parameters]
To see help text, you can run:
aws help
aws <command> help
aws <command> <subcommand> help
Unknown options: {"S":, "CLI"},"id":, {"N":, 20220920001},"sender":, {"S":, "Bob"},"message":, {"S":, "From, CLI"},"timetag":, {"S":, "202209201400123"}, }', "group":
引用符とエスケープが原因です。内容やエスケープを確認する必要があります。
Parameter validation failed:
エラーメッセージ:
Parameter validation failed:
Invalid type for parameter Item.id.N, value: 20220920001, type: <class 'int'>, valid types: <class 'str'>
これは、データ項目JSONのデータ型が間違っていることが原因です。データ型が数値であっても、データ項目JSONのデータ値はクォーテーションで囲まれた文字列でなければならないことに注意します。そうでない場合は、このパラメータ検証エラーが発生します。
Node.js SDKとPython SDKでクレデンシャルを設定するには、いくつかの方法があります。今回のデモではAWS CLI、Node.js SDK、Python SDKを同時に使用しているため、推奨される方法は共有ファイルからクレデンシャルを読み込む方法です。
デフォルトでは、~/.aws/credentialsがその場所です。最低限、クレデンシャルファイルにはアクセスキーとシークレットアクセスキーを指定する必要があります。
[default]
aws_access_key_id = YOUR_ACCESS_KEY
aws_secret_access_key = YOUR_SECRET_KEY
もし、共有認証情報ファイルをどのように準備し、配置すればよいかわからない場合は、`aws configure`コマンドを使用して生成することができます。
属性名がDynamoDBの予約語と衝突することがあります。その場合、以下のようなエラーメッセージが表示されます。
Invalid FilterExpression: Attribute name is a reserved keyword; reserved keyword: group
この場合、パーティションキー `group` が予約語と衝突するので、ExpressionAttributeNames を使用して問題を解決する必要があります。
……
var params = {
FilterExpression: "#group = :g",
ExpressionAttributeValues: {
":g": group,
},
ExpressionAttributeNames: { "#group": "group" },
TableName: table,
};
……
Amazon Web Service DynamoDBとNode.js もしくは Pythonで始めるRESTful APIサービス構築方法をご紹介しました。この方法は初心者向け記事としても、チャットサービス(IM Service)向けにも適用できますし、10分もせずにRESTful APIサービスを完成することができます。
DynamoDB は NoSQLデータベースとして、Webやモバイルなどのアプリケーションから簡単にアクセスできるよう、Node.jsやPythonなど幅広い言語でのクライアントを提供しています。そのため、どんな言語でも通信プロトコルを意識せずにRESTful APIサービスとして展開することができます。RESTful APIサービスを構築してみたい方は是非参考にしてみるといいでしょう。
条件に該当するページがございません