迷ったらカレー

技術系の雑多なブログ

Serverless FrameworkとExpressでお手軽にAPIサーバを作ろう

この記事は、Recruit Engineers Advent Calendar 20176日目の記事です。

こんにちは。最近マルチロールな働き方をしていて自分が何者なのかわからなくなってきた加藤です。現在リクルートジョブズというところでエンジニアをしています。

日頃、社内用のちょっとしたツールとか、試験的なAPIとかを作るのにAWS LambdaやDynamoDBを使ってサーバレスな開発をしているのですが、この記事ではその際によくやるServerless FrameworkとExpressを活用した開発手法の紹介をしたいと思います。


Expressといえば、Node.jsのWebアプリケーションフレームワークとして有名ですが、最近ではaws-serverless-expressというのを使うと、AWS LambdaでもExpressを使って開発ができます。サラッと書きましたが、正直こいつが出てきた時僕は100万starくらいしたい気持ちでした。そのくらいlambdaを便利にしてくれたと思っています。

github.com

一方、Serverless Frameworkというと「?」となる方も多いかと思います。ざっくり説明すると、API GatewayAWS Lambdaなどのフルマネージドなサービスを組み合わせる、 サーバレス・アーキテクチャ による開発を簡単にしてくれるツールです。AWSには標準でCloud Formationというオーケストレーション機能がありますが、イメージとしては、Cloud FormationにわかりやすいCUIと拡張性のある開発環境がくっついたような感じです。プラグインを使うとローカルにAPI GatewayやLambda、S3、DynamoDBなどの開発環境を立ててることができるようになっています。

serverless.com

これら二つを組み合わせて使うと、簡単にサーバレスなAPIの開発ができるというお話です。

つくるもの

要件

よくありそうなTODOアプリのAPIをつくります。

機能 メソッド パス
一覧 GET /items
追加 POST /item
取得 GET /item/:id
更新 PUT /item/:id
削除 DELETE /item/:id

構成

API Gateway + Lambda + DynamoDBというベーシックな構成です。

ソースコード

github.com

前提条件

  • AWS CLIの設定が完了していること
  • node.jsがインストールされていてnpmコマンドが使えること
  • https://serverless.com/に登録していること

手順

1. 準備

serverlessのセットアップ

まず、npmでserverlessをグローバルに入れます。

$ npm install -g serverless

これで、serverless,slss,slsコマンドが使えるようになります。

$ sls

サブコマンドを指定しないで呼ぶと、ヘルプメニューとして使えるコマンド一覧が表示されます。この記事でもこの先何回もslsコマンドを使っていくことになります。 このタイミングで、Serverless.comにログインしておきます。

$ sls login

ブラウザが開くはずなので、よしなにログイン処理をすすめてください。

プロジェクトの初期化

$ mkdir sls-todo && cd sls-todo
$ npm init

必要に応じて、.gitignoreもよしなに書いてください。 さて、この手順の仕上げとして、今回必要になるファイルとディレクトリをあらかじめ設置しておきます。以下のような構造にしました。

.
├── package.json
├── seeds
│   └── todo-items.json
├── serverless.yml
└── src
    ├── app.js
    └── routes
        └── index.js

ここで、serverless.ymlというファイルを作成しました。これは、Serverless Frameworkの設定ファイルです。Serverless Frameworkはこのファイルに書かれている内容を実行していきますので、アプリケーションの本体といっても過言ではないかもしれません。3. serverless.ymlを書くに、完成形のserverless.ymlを載せてあります。

2. 使うもののインストール

Lambdaで使うやつ

  • aws
  • aws-serverless-express
  • express
  • body-parser
  • cors
  • compression
$ npm install --save express aws-sdk aws-serverless-express body-parser cors compression

Serverlessプラグイン

  • serverless
  • serverless-offline
  • serverless-dynamodb-local
  • serverless-plugin-include-dependencies
$ npm install --save-dev serverless serverless-offline serverless-dynamodb-local serverless-plugin-include-dependencies

3. serverless.ymlを書く

serverless.ymlを書くのは、最初はちょっと骨が折れるかもしれません。ボトムアップに作っていくのは結構きついものがあるので、ほぼ全部入りのUser GuideのServerless.ymlから、自分が使いたい部分を抜き出してきて使うのがオススメです。各リソースのイベントについて細かい設定がしたい場合、Eventsの各ページが参考になります。

今回は以下のように設定を書きました。各項目の内容はコメントに書いてある通りです。

service: sls-todos

# 実行環境に関する設定
provider:
  name: aws
  runtime: nodejs6.10
  region: ap-northeast-1

# 利用するプラグインについて
plugins:
  - serverless-offline
  - serverless-dynamodb-local
  - serverless-plugin-include-dependencies

# デプロイ時のパッケージの内容について
package:
  include:
    - src/**

# lambdaを実行するトリガの設定。今回はAPI Gatewayのルーティング設定
functions:
  sls-todo:
    role: ${DYNAMO_ACCESS_ROLE} # dynamoにアクセスできるロール
    description: TODOリストAPIのルーティング
    handler: src/routes/index.handler
    events:
      - http:
          path: /todos
          method: get
          cors: true
      - http:
          path: /todo
          method: post
          cors: true
      - http:
          path: /todo/{id}
          method: get
          cors: true
          request:
            parameters:
              paths:
                id: true
      - http:
          path: /todo/{id}
          method: put
          cors: true
          request:
            parameters:
              paths:
                id: true
      - http:
          path: /todo/{id}
          method: delete
          cors: true
          request:
            parameters:
              paths:
                id: true

# ローカルのdynamodbの起動設定
custom:
  dynamodb:
    start:
      port: 8888
      inMemory: true
      migrate: true
      seed: true
    seed:
      test:
        sources:
          - table: sls-todos
            sources: [./seeds/todo-items.json]

# CloudFormationの形式でのリソース定義
resources:
  Resources:
    slsTodos:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: sls-todos
        AttributeDefinitions:
          - AttributeName: id
            AttributeType: S
        KeySchema:
          - AttributeName: id
            KeyType: HASH
        ProvisionedThroughput:
          ReadCapacityUnits: 1
          WriteCapacityUnits: 1

4. DynamoDBのローカルへのインストールと起動

serverless.ymlpluginsを書いたことで、プラグインが提供するコマンドが使えるようになっています。

slsと打つと、以下のようなコマンドが一覧に追加されて表示されるはずです。

dynamodb ...................... undefined
dynamodb migrate .............. Creates local DynamoDB tables from the current Serverless configuration
dynamodb seed ................. Seeds local DynamoDB tables with data
dynamodb start ................ Starts local DynamoDB
dynamodb remove ............... Removes local DynamoDB
dynamodb install .............. Installs local DynamoDB
$ sls dynamodb install

これで、ローカルにDynamoDBがインストールされます。 DBの準備ができたので、今度はseedファイルの準備をします。

seeds/todo-items.json

[
  {
    "id" : "test",
    "todo" : "Hello World!"
  }
]

seedファイルの準備ができたら、dynamodbを起動します。

$ sls dynamodb start

先ほど、serverless.ymlのdynamodbパートでの起動時オプションにseed: truemigrate: trueをつけたので、起動と同時にseedファイルの内容がDBに反映されます。

5. ExpressでAPIを書く

使っているミドルウェアは少し違いますが、基本的には普通にExpressを利用するときと同じ書き方ができます。ここでは、ルーティングに関わらない共通の処理をapp.jsにまとめています。

src/app.js

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');
const compression = require('compression');
const awsServerlessExpressMiddleware = require('aws-serverless-express/middleware');
const app = express();

app.set('view engine', 'pug');
app.use(compression());
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(awsServerlessExpressMiddleware.eventContext());

module.exports = app;

一つ完全に違うのはapp.jsroutes以下のルーターを読むのではなく、ルーターapp.jsを読み込んでいるという点です。これは、app.jsを核にして一つのサービスが立つのではなく、各ルーターが独立したサービスとして立ちapp.jsという共通設定を利用する形になっているためです。

src/routes/index.js

const express = require("express");
const awsServerlessExpress = require('aws-serverless-express');
const AWS = require("aws-sdk");
const app = require("../app");
const router = express.Router();

//ローカルかリモートかで、dynamodbのconfigを切り替える
const dynamoConfig = process.env.NODE_ENV === "development" ? {
  region: "localhost",
  endpoint: "http://localhost:8888", 
} : {};
const ddc = new AWS.DynamoDB.DocumentClient(dynamoConfig);

//一覧
router.get('/todos', (req, res, next) => {
  ddc.scan({
    TableName: "sls-todos"
  }).promise()
  .then((result) => res.send(result))
  .catch(next);
});

//追加
router.post('/todo', (req, res, next) => {
  ddc.put({
    TableName: "sls-todos", 
    Item: req.body.Item
  }).promise()
  .then((result) => res.send(result))
  .catch(next);
});

//取得
router.get('/todo/:id', (req, res, next) => {
  ddc.get({
    TableName: "sls-todos",
    Key:{
      id: req.params.id,
    }
  }).promise()
  .then((result) => res.send(result))
  .catch(next);
});

//更新
router.put('/todo/:id', (req, res, next) => {
  req.body.Item.id = req.params.id;
  ddc.get({
    TableName: "sls-todos",
    Item: req.body.Item
  }).promise()
  .then((result) => res.send({ message: "update success!" }))
  .catch(next);
});

//削除
router.delete('/todo/:id', (req, res, next) => {
  ddc.delete({
    TableName: "sls-todos",
    Key:{
      id: req.params.id,
    }
  }).promise()
  .then((result) => res.send({ message: "delete success!" }))
  .catch(next);
});

router.use((err, req, res, next) => {
  res.status(500).send(err);
});

app.use("/", router);

const server = awsServerlessExpress.createServer(app);
exports.handler = (event, context) => awsServerlessExpress.proxy(server, event, context);

6. ローカルで動かす

ここまでで、アプリケーションの設定とコードの準備が全て整いました。offlineコマンドで、ローカルにサーバを立ち上げてみましょう。

$ sls offline
Serverless: Starting Offline: dev/ap-northeast-1.

Serverless: Routes for sls-todo:
Serverless: GET /todos
Serverless: POST /todo
Serverless: GET /todo/{id}
Serverless: PUT /todo/{id}
Serverless: DELETE /todo/{id}

Serverless: Offline listening on http://localhost:3000

localhost:3000でサーバが立ち上がります。serverless.ymlに設定したルーティングに対して、リクエストを投げてみましょう。curlやブラウザでやるのもいいですが、個人的にはpostmanがオススメです。

7. デプロイする

$ sls deploy

これだけです。ローカルの時と同じく、デプロイされた先のAPIを叩いてみましょう。

$ sls remove

挙動の確認ができて、必要がなくなったら畳んでしまいましょう。リモートにデプロイしたまま放置していると、知らないうちにお金がかかっていることがあるので...。

まとめ

  • Serverless frameworkを使うと嬉しいことがたくさん
    • フルマネージドなサービスを使ってサクッとAPIを作れる
    • APIをつくるときにExpressを使うとコードも書きやすくてなおよい
    • プラグインを使えばローカルに検証環境を作るのも簡単
    • デプロイするのも、逆に落とすのもコマンド一つでOK
    • ただしserverless.ymlを書くのだけはがんばってください

それではみなさま、よきサーバレスライフを!!