Edited at

serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)

More than 1 year has passed since last update.


serverless frameworkを使って本格的なAPIサーバーを構築(ハンズオン編)

最近、推しメンの serverless framework の記事の第2弾です。

保守メンテが楽になりつつも、実戦で速攻で構築ができます。


目次

framework_repo.png

前回、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 など適当に変えれば良いです。


handler.js

'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を追加する




serverless.yml

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です!!!

スクリーンショット 2017-09-23 15.28.55.png

スクリーンショット 2017-09-23 15.28.38.png

このように、GETでアクセスをするとさっき作成したJSONを返していることがわかります。

完璧だー 🍙🍙🍙


REST APIを作ってみる


  • 次は、DynamoDBと連携 して、get post put deleteを実装してみます。


  • serverless.ymlを編集します


  • REST APIのサンプルは、aws-node-rest-api-with-dynamodbにあります。



serverless.yml


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を生成しています。



次に、それぞれのLambda関数を作成します


create.js


todos/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


todos/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


todos/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


todos/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


todos/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 すると確認ができるはずです。


スケジュールを設定して定期実行してみる


serverless.yml

  hoge:

handler: functions/aggregate/index.handler
events:
- schedule:
rate: cron(0 1 * * ? *)



serverless.yml

  hoge:

handler: functions/aggregate/index.handler
events:
- schedule:
rate: rate(5 minutes)


DynamoStreamを使ってputされたときに何か実行してみる


  • DynamoStreamとは、DynamoDBにputやupdateがあった場合に、そのイベントをトリガーにLambdaでまた処理させることができます。


  • DynamoのResourcesにStreamViewType を追加



serverless.yml

    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関数が呼ばれます。


serverless.yml

  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を使うとかんたんにAPIが作成できました!!

  • イベントの作成やオプションもまったく困らないと思います。

  • サンプルやプラグインが豊富で、ベストプラクティスには迷いません!


余談


  • 前回serverlessの魅了の紹介で、serverlessを使わないほうがいいときはないのかという質問を受けました。


serverlessを使わないほうがいいとき


  • 複雑なクエリを要するアプリを作るとき


    • 複雑なクエリを要するアプリを作るときは、DynamoDBでも設計を入念に行えばいいかと思いますが、whereなどのクエリが使えないので注意が必要です

    • MySQLクライアントもLambdaで使用することができますが、若干使いにくいようです。



  • Dynamoに書き込む容量が大きいとき




次回