Serverless Frameworkで DynamoDB を使ってみます。
参考
-
https://github.com/pmuens/serverless-crud
- これをベースに真似ています。
-
https://github.com/jch254/serverless-es6-dynamodb-webapi
- babel で ES6 を使う場合、これをベースに真似るのが良いと思いました。この作りは好きです。
-
https://github.com/99xt/serverless-react-boilerplate
- 参考になりますが、これを真似るとかなり凝った作りになると思います。
ソースリポジトリ
ソースは以下のリポジトリを参照してください。
https://github.com/katsuhiko/sls-dynamodb
バージョン
version |
---|
node 6.9.1 |
npm 3.10.8 |
serverless framework 1.1.0 |
Lambda が Node.js V4.3 なので、合わせるべきかもしれません。今はローカルでは動かしていないので合わしていません。
DynamoDB を使う
todos.js
気軽に使いたいので、あまりレイヤは設けたくはないのですが、Modelに相当する部分のみを todos.js
として切り出しました。
Lambda と DynamoDB を意識せずに作れると良いけど、がっつり DynamoDB を意識している作りです。
「moment」は、日付を扱いやすくするライブラリです。
ユニークなキーを生成するのに「uuid」を使っています。「chance」でも良いと思います。
DynamoDB のテーブル名にステージを prefix として追加しています。やり方は他にもあるかもしれませんが、各ステージごとに DynamoDB テーブルを作る仕組みを盛り込まないと実際に利用するときに困ると思います。今回は、ステージを prefix としてテーブル名につけることで対応しました。
Javascript からステージを参照できるようにするために Plugin「serverless-plugin-write-env-vars」と「dotenv」を使っています。コメントによると Serverless Framework で似た機能が提供されるらしいので、いずれ不要になると思います。
'use strict';
const uuid = require('uuid'),
moment = require('moment'),
tableName = `${process.env.STAGE}-todos`;
module.exports.readAll = (db, callback) => {
const params = {
TableName: tableName
};
return db.scan(params, (err, data) => {
if (err) {
callback(err);
} else {
callback(err, data.Items);
}
});
};
module.exports.readOne = (db, id, callback) => {
const params = {
TableName: tableName,
Key: {
id: id
}
};
return db.get(params, (err, data) => {
if (err) {
callback(err);
} else {
callback(err, data.Item);
}
});
};
module.exports.create = (db, data, callback) => {
data.id = uuid.v1();
data.updatedUtc = moment().utc().toISOString();
const params = {
TableName: tableName,
Item: data
};
return db.put(params, (err, data) => {
if (err) {
callback(err);
} else {
callback(err, params.Item);
}
});
};
module.exports.update = (db, id, data, callback) => {
data.id = id;
data.updatedAt = moment().utc().toISOString();
const params = {
TableName : tableName,
Item: data
};
return db.put(params, (err, data) => {
if (err) {
callback(err);
} else {
callback(err, params.Item);
}
});
};
module.exports.delete = (db, id, callback) => {
const params = {
TableName : tableName,
Key: {
id: id
}
};
return db.delete(params, (err, data) => {
if (err) {
callback(err);
} else {
callback(err, params.Key);
}
});
};
handler.js
Lambda Proxy に対応した返却を行っています。context
ではなく callback
を使っています。しっかりした情報を得ることはできませんでしたが、callback
を使うのが良いのではないかと思います。
極力シンプルにしたいですが、HTTPステータスを返却するところがどうしても IF が入ってキレイにできなかったです。
'use strict';
const AWS = require('aws-sdk'),
dynamoDb = new AWS.DynamoDB.DocumentClient(),
env = require('dotenv').config(),
todos = require('./todos.js');
const createResponse = (statusCode, body) => (
{
statusCode,
headers: {
'Access-Control-Allow-Origin': '*', // Required for CORS support to work
},
body: JSON.stringify(body),
}
);
module.exports.todosReadAll = (event, context, callback) => {
todos.readAll(dynamoDb, (err, result) => {
if (err) {
callback(createResponse(500, { message: err.message }));
} else {
callback(null, createResponse(200, result));
}
});
};
module.exports.todosReadOne = (event, context, callback) => {
const id = event.pathParameters.id;
todos.readOne(dynamoDb, id, (err, result) => {
if (err) {
callback(createResponse(500, { message: err.message }));
} else if (!result) {
callback(null, createResponse(404, { message: 'not found'}));
} else {
callback(null, createResponse(200, result));
}
});
};
module.exports.todosCreate = (event, context, callback) => {
const data = JSON.parse(event.body);
todos.create(dynamoDb, data, (err, result) => {
if (err) {
callback(createResponse(500, { message: err.message }));
} else {
callback(null, createResponse(201, result));
}
});
};
module.exports.todosUpdate = (event, context, callback) => {
const id = event.pathParameters.id,
data = JSON.parse(event.body);
todos.update(dynamoDb, id, data, (err, result) => {
if (err) {
callback(createResponse(500, { message: err.message }));
} else {
callback(null, createResponse(200, result));
}
});
};
module.exports.todosDelete = (event, context, callback) => {
const id = event.pathParameters.id;
todos.delete(dynamoDb, id, (err, result) => {
if (err) {
callback(createResponse(500, { message: err.message }));
} else {
callback(null, createResponse(204));
}
});
};
serverless.yml
iamRoleStatements
と Resources
を使って DynamoDB へアクセスする権限とテーブル作成を行っています。
この設定で作られた DynamoDB テーブルは DeletionPolicy: Retain
を指定しているので、sls remove
を実行しても削除されません。
不要な場合、AWS Console 等を使って別途削除する必要があります。
service: sls-dynamodb
provider:
name: aws
runtime: nodejs4.3
stage: ${opt:stage, self:custom.defaultStage}
region: ${opt:region, self:custom.defaultRegion}
profile: ${self:custom.profiles.${self:provider.stage}}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:DescribeTable
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.stage}-*"
custom:
defaultStage: dev
defaultRegion: ap-northeast-1
profiles:
dev: devSls
prod: prodSls
writeEnvVars:
STAGE: ${self:provider.stage}
package:
exclude:
- .git/**
- README.md
- node_modules/serverless-plugin-write-env-vars/**
plugins:
- serverless-plugin-write-env-vars
functions:
todosReadAll:
handler: handler.todosReadAll
events:
- http:
path: todos
method: get
cors: true
todosReadOne:
handler: handler.todosReadOne
events:
- http:
path: todos/{id}
method: get
cors: true
todosCreate:
handler: handler.todosCreate
events:
- http:
path: todos
method: post
cors: true
todosUpdate:
handler: handler.todosUpdate
events:
- http:
path: todos/{id}
method: patch
cors: true
todosDelete:
handler: handler.todosDelete
events:
- http:
path: todos/{id}
method: delete
cors: true
resources:
Resources:
TodosDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
-
AttributeName: id
AttributeType: S
KeySchema:
-
AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: "${self:provider.stage}-todos"
サンプルを動かしてみる
開発を始めるための Serverless Framework のインストール等については「Serverless Framework で Hello World を作る」を参照してください。
サンプルのインストール/デプロイ
Serverless Framework のインストールと AWS への接続ができていたら、以下のコマンドでサンプルをインストール/デプロイできます。
serverless install --url https://github.com/katsuhiko/sls-dynamodb
cd sls-dynamodb
npm install
serverless deploy -v
実行
curl を使って実行します。「XXXX」部分は各環境に合わして変更してください。
Read all
curl https://XXXX.execute-api.ap-northeast-1.amazonaws.com/dev/todos
Read one
curl https://XXXX.execute-api.ap-northeast-1.amazonaws.com/dev/todos/<id>
Create
curl -X POST https://XXXX.execute-api.ap-northeast-1.amazonaws.com/dev/todos --data '{ "content" : "Learn Serverless" }'
Update
curl -X PATCH https://XXXX.execute-api.ap-northeast-1.amazonaws.com/dev/todos/<id> --data '{ "content" : "Understand Serverless" }'
Delete
curl -X DELETE https://XXXX.execute-api.ap-northeast-1.amazonaws.com/dev/todos/<id>
あと片付け
serverless remove -v
上記でも DynamoDB テーブルが残っているので、AWS Console 等から DynamoDB テーブルを削除します。
感想
DynamoDB を使うのは簡単でした。
気になった点は1点。
Lambda にデプロイされる zip の node_modules に plugin に関するソースも含まれる点です。(細かいですが。)
serverless.yml で package.exclude していますが、package.json の devDependencies は含めないようにする仕組みが欲しいと思いました。