Help us understand the problem. What is going on with this article?

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に書き込む容量が大きいとき

次回

okamu_
no plan inc. CEO 元フリーランスエンジニア/ iOS / サーバーサイド / 共同創業 / 福岡出身 https://qiita.com/organizations/noplan-inc
https://twitter.com/okamu_ro
admin-guild
「Webサービスの運営に必要なあらゆる知見」を共有できる場として作られた、運営者のためのコミュニティです。
https://admin-guild.slack.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした