serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)
最近、推しメンの serverless framework の記事の第2弾です。
保守メンテが楽になりつつも、実戦で速攻で構築ができます。
目次
- serverless frameworkを使って本格的なAPIサーバーを構築(魅力編)
- serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)← 今ここ
- serverless frameworkを使って本格的なAPIサーバーを構築(Express編)
- serverless frameworkを使って本格的なAPIサーバーを構築(テストコード編)
前回、serverless frameworkの魅力の記事を書きました。
今回は、serverless frameworkで 「 lambda + APIGateway + DynamoDB 」 の構成で簡単なサンプルアプリを作成します。
この記事でできるようになること
- REST FULLなAPIを構築する
- DynamoDBと連携できる
- スケージューリングで実行する
- DynamoStreamが使えるようになる
前準備
- serverlessをinstallしておく
$ npm install -g serverless
- プロジェクトを生成する
$ serverless create --template aws-nodejs --path my-service
すると、以下のファイルが出来ているはずです。
$ ls
serverless.yml
handler.js
また、サービス用のテンプレートですが、以下の言語用のテンプレートが用意されています。
使える言語
aws-nodejs
aws-python
aws-java-maven
aws-java-gradle
aws-scala-sbt
まずは、単純にJSONを返してみる
- 最初はHelloWorld!をJSONで返すAPIで表示してみます。
-
serverless create
をした時点でhandler.jsはこのようになっているので、messageをhello world
など適当に変えれば良いです。
'use strict';
module.exports.hello = (event, context, callback) => {
const response = {
statusCode: 200,
body: JSON.stringify({
message: 'Go Serverless v1.0! Your function executed successfully!',
input: event,
}),
};
callback(null, response);
// Use this code if you don't use the http event with the LAMBDA-PROXY integration
// callback(null, { message: 'Go Serverless v1.0! Your function executed successfully!', event });
};
- 最初のserverless.ymlを少し編集します
- regionがdefaultでは
us-east-1
なのでap-northeast-1
にする - getでアクセスできるように eventsを追加する
- regionがdefaultでは
service: my-service
provider:
name: aws
runtime: nodejs6.10
stage: dev
region: ap-northeast-1
functions:
hello:
handler: handler.hello
events:
- http:
path: hello
method: get
- これでOKです!! さっそく、deployしてみましょう!!!!!!
$ sls deploy -v -s dev
-
-v
は進捗を表示 -
-s
はステージの名前を指示します(defaultはdev) - APIGatewayでは、プロジェクトからステージごとにデプロイしていましたが、serverlessではステージごとにプロジェクトが作られます。
Serverless: Stack update finished...
Service Information
service: XXXXXXXX
stage: dev
region: ap-northeast-1
api keys:
None
endpoints:
GET - https://XXXXXXXX.ap-northeast-1.amazonaws.com/dev/hello
functions:
hello: XXXXXXXX
Stack Outputs
Serverless: Removing old service versions...
デプロイが成功すると、 APIGateWayには設定済みのプロジェクトと、endpoints
を教えてくれるのでそこにアクセスしてみてhelloとJSONが表示されればOKです!!!
このように、GETでアクセスをするとさっき作成したJSONを返していることがわかります。
完璧だー 🍙🍙🍙
REST APIを作ってみる
-
次は、DynamoDBと連携 して、get post put deleteを実装してみます。
-
serverless.ymlを編集します
-
REST APIのサンプルは、aws-node-rest-api-with-dynamodbにあります。
serverless.yml
service: serverless-rest-api-with-dynamodb
frameworkVersion: ">=1.1.0 <2.0.0"
provider:
name: aws
runtime: nodejs4.3
environment:
DYNAMODB_TABLE: ${self:service}-${opt:stage, self:provider.stage}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Query
- dynamodb:Scan
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource: "arn:aws:dynamodb:${opt:region, self:provider.region}:*:table/${self:provider.environment.DYNAMODB_TABLE}"
functions:
create:
handler: todos/create.create
events:
- http:
path: todos
method: post
cors: true
list:
handler: todos/list.list
events:
- http:
path: todos
method: get
cors: true
get:
handler: todos/get.get
events:
- http:
path: todos/{id}
method: get
cors: true
update:
handler: todos/update.update
events:
- http:
path: todos/{id}
method: put
cors: true
delete:
handler: todos/delete.delete
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.environment.DYNAMODB_TABLE}
- serverless.ymlのポイント
- environmentで、どのLambdaからでも
DYNAMODB_TABLE
を参照することができます。 - todosというフォルダの中に create.js get.js list.js delete.js update.jsを置いておきます。
- resourcesでDynamoDBを生成しています。
- environmentで、どのLambdaからでも
次に、それぞれのLambda関数を作成します
create.js
'use strict';
const uuid = require('uuid');
const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.create = (event, context, callback) => {
const timestamp = new Date().getTime();
const data = JSON.parse(event.body);
if (typeof data.text !== 'string') {
console.error('Validation Failed');
callback(null, {
statusCode: 400,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t create the todo item.',
});
return;
}
const params = {
TableName: process.env.DYNAMODB_TABLE,
Item: {
id: uuid.v1(),
text: data.text,
checked: false,
createdAt: timestamp,
updatedAt: timestamp,
},
};
// write the todo to the database
dynamoDb.put(params, (error) => {
// handle potential errors
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t create the todo item.',
});
return;
}
// create a response
const response = {
statusCode: 200,
body: JSON.stringify(params.Item),
};
callback(null, response);
});
};
delete.js
'use strict';
const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.delete = (event, context, callback) => {
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: {
id: event.pathParameters.id,
},
};
// delete the todo from the database
dynamoDb.delete(params, (error) => {
// handle potential errors
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t remove the todo item.',
});
return;
}
// create a response
const response = {
statusCode: 200,
body: JSON.stringify({}),
};
callback(null, response);
});
};
get.js
'use strict';
const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.get = (event, context, callback) => {
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: {
id: event.pathParameters.id,
},
};
// fetch todo from the database
dynamoDb.get(params, (error, result) => {
// handle potential errors
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t fetch the todo item.',
});
return;
}
// create a response
const response = {
statusCode: 200,
body: JSON.stringify(result.Item),
};
callback(null, response);
});
};
list.js
'use strict';
const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
const params = {
TableName: process.env.DYNAMODB_TABLE,
};
module.exports.list = (event, context, callback) => {
// fetch all todos from the database
dynamoDb.scan(params, (error, result) => {
// handle potential errors
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t fetch the todos.',
});
return;
}
// create a response
const response = {
statusCode: 200,
body: JSON.stringify(result.Items),
};
callback(null, response);
});
};
update.js
'use strict';
const AWS = require('aws-sdk'); // eslint-disable-line import/no-extraneous-dependencies
const dynamoDb = new AWS.DynamoDB.DocumentClient();
module.exports.update = (event, context, callback) => {
const timestamp = new Date().getTime();
const data = JSON.parse(event.body);
// validation
if (typeof data.text !== 'string' || typeof data.checked !== 'boolean') {
console.error('Validation Failed');
callback(null, {
statusCode: 400,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t update the todo item.',
});
return;
}
const params = {
TableName: process.env.DYNAMODB_TABLE,
Key: {
id: event.pathParameters.id,
},
ExpressionAttributeNames: {
'#todo_text': 'text',
},
ExpressionAttributeValues: {
':text': data.text,
':checked': data.checked,
':updatedAt': timestamp,
},
UpdateExpression: 'SET #todo_text = :text, checked = :checked, updatedAt = :updatedAt',
ReturnValues: 'ALL_NEW',
};
// update the todo in the database
dynamoDb.update(params, (error, result) => {
// handle potential errors
if (error) {
console.error(error);
callback(null, {
statusCode: error.statusCode || 501,
headers: { 'Content-Type': 'text/plain' },
body: 'Couldn\'t fetch the todo item.',
});
return;
}
// create a response
const response = {
statusCode: 200,
body: JSON.stringify(result.Attributes),
};
callback(null, response);
});
};
これもデプロイをしてみてください!!
postmanなどで GET POST PUT DELETE すると確認ができるはずです。
スケジュールを設定して定期実行してみる
-
Lambdaの定期実行の形式をserverless.ymlにそのまま記述することもできます。
hoge:
handler: functions/aggregate/index.handler
events:
- schedule:
rate: cron(0 1 * * ? *)
hoge:
handler: functions/aggregate/index.handler
events:
- schedule:
rate: rate(5 minutes)
DynamoStreamを使ってputされたときに何か実行してみる
-
DynamoStreamとは、DynamoDBにputやupdateがあった場合に、そのイベントをトリガーにLambdaでまた処理させることができます。
-
DynamoのResourcesに
StreamViewType
を追加
HogeDynamoDbTable:
Type: 'AWS::DynamoDB::Table'
DeletionPolicy: Retain
Properties:
AttributeDefinitions:
-
AttributeName: column
AttributeType: S
KeySchema:
-
AttributeName: column
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 1
WriteCapacityUnits: 1
TableName: "hoge-${self:provider.stage}"
StreamSpecification:
StreamViewType: KEYS_ONLY
そして、Streamでどの関数が呼ばれるかを指定します
以下のようにすると、二つのDynamoDBに変更がある場合に1つのLambda関数が呼ばれます。
hoge:
handler: functions/hoge/index.handler
events:
- stream:
type: dynamodb
arn:
Fn::GetAtt:
- HogeDynamoDbTable
- StreamArn
batchSize: 1
- stream:
type: dynamodb
arn:
Fn::GetAtt:
- HogeDynamoDbTable2
- StreamArn
batchSize: 1
batchSizeは1度にどれだけ項目がほしいか設定できます。
ちなみに、StreamViewTypeには
KEYS_ONLY => HASHキーのみ関数で取得できる
NEW_IMAGE => 新しいものだけ取得できます
OLD_IMAGE => 古いものだけ取得できます
NEW_AND_OLD_IMAGES => 新旧のデータが取得できます
があります。
その他できること
- serverlessで、SNS IoT S3 Kinesisのイベントなどをトリガーとして発動させることができます。
- https://serverless.com/framework/docs/providers/aws/events/
まとめ
- serverlessを使うとかんたんにAPIが作成できました!!
- イベントの作成やオプションもまったく困らないと思います。
- サンプルやプラグインが豊富で、ベストプラクティスには迷いません!
余談
- 前回serverlessの魅了の紹介で、serverlessを使わないほうがいいときはないのかという質問を受けました。
serverlessを使わないほうがいいとき
- 複雑なクエリを要するアプリを作るとき
- 複雑なクエリを要するアプリを作るときは、DynamoDBでも設計を入念に行えばいいかと思いますが、whereなどのクエリが使えないので注意が必要です
- MySQLクライアントもLambdaで使用することができますが、若干使いにくいようです。
- Dynamoに書き込む容量が大きいとき
- DynamoDBはとても安いですが、書き込み読み込みのキャパシティが多くなると料金が結構高くなりますので注意が必要です。
- https://www.slideshare.net/AmazonWebServicesJapan/20150805-aws-blackbeltdynamodb
- https://qiita.com/YU81/items/e1e336990ed8cfb938d9