Node.jsでLambdaを開発するならClaudia.jsがオススメ

  • 31
    いいね
  • 0
    コメント

この記事はServerless(2) Advent Calendar 2016 の16日目の記事です。

自身のプロダクトをClaudia.jsを使って開発しているのですが、今回はその紹介を書きたいと思います。

Claudia.jsとは

Claudia.jsはオープンソースのLambdaデプロイメントツールです。Node.jsで作成したLambdaアプリケーションを簡易にデプロイすることが出来ます。API Gatewayと組み合わせることも可能です。

Lambdaのデプロイツール(フレームワーク)といえばServerless FrameworkやApexなど多数存在しますが、他のツールとくらべてClaudia.jsは何が違うのでしょうか?

最も大きな特徴は、Node.jsに特化しているという点です。ServerlessやApexのように複数のプログラミング言語に対応しているツールと異なり、Claudia.jsはNode.jsしかサポートしていません。その分、プログラミング言語に踏み込んだ機能が提供されているため、開発者は必要最小限のコードで開発を行うことができます。

作者のGojko Adzic氏はインタビューで以下のように話しています。

汎用的なフレームワークならばもっと多くのランタイムをサポートしますが,言語固有の部分への対処は開発者に任されています。それに,LambdaでJavaScriptを実行させて気が付いたのですが,言語固有の問題というのは実にたくさんあるのです。

ClaudiaはJavaScript/Node.jsでのみ動作しますが,それに特化しています。Node.jsに重点を置いているため,パラメータと結果をJavaScriptで処理しやすいオブジェクトに変換するテンプレートが,自動的にインストールされます。JavaScript開発者が希望する動作が,アウトオブボックスで可能になっているのです。例えばHTTPコード200で通知されるエラーは,Javaでは大きな問題ではありませんが,大部分のJavaScript Promise HTTPライブラリの動作を損ないます。そのためClaudisaでは,HTTP 500を返送するAPIゲートウェイを自動的にセットしています。

Clauda.jsでNode.jsマイクロサービスをAWS Lambdaにデプロイする - 作者Gojko Adzic氏とのQ&A

Claudia.js自体はフレームワークではなく、シンプルなコマンドラインによるデプロイメントツールですが、API Builderという拡張ライブラリを使うことにより、API GatewayとLambdaを組み合わせたWeb APIを簡易に作成することが可能になります。

では実際に使用して試してみたいと思います。

単独のLambda Functionを作成してみる

まずシンプルなLambda Functionのデプロイを試してみます。

コマンドラインツールをインストール

$ npm install claudia -g
$ claudia --version
2.3.0

Lambdaプロジェクト作成

プロジェクトフォルダを作成し、以下のファイルを用意します。

mkdir simple-lambda
cd simple-lambda
package.json
  "name": "simple-lambda",
  "version": "1.0.0",
  "description": "",
  "main": "lambda.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
event.json
{
  "name": "Claudia"
}
lambda.js
exports.handler = function (event, context) {
    console.log(event);
    context.succeed('hello ' + event.name);
};

Lambda Functionのコードは通常のLambda Functionを作成するときのコードと同じで、特に書き方は変わりません。

Lambda Functionのデプロイ

claudia create でLambda Functionの作成を行います。

$ claudia create --region ap-northeast-1 --handler lambda.handler
saving configuration
{
  "lambda": {
    "role": "simple-lambda-executor",
    "name": "simple-lambda",
    "region": "ap-northeast-1"
  }
}

Lambda Functionの実行

作成したFunctionは claudia test-lambda で実行できます。

$ claudia test-lambda --event event.json
{
  "StatusCode": 200,
  "Payload": "\"hello claudia!!\""
}

event.jsonで渡したパラメータを元に結果が返って来ているのがわかります。

Lambda Functionの更新

更新は、claudia updateで行えます。試しにversionをつけて更新してみます。

claudia update --version test-version

setting version alias   lambda.createAlias  FunctionName=simple-lambda  Name=test-version
{
  "FunctionName": "simple-lambda",
  "FunctionArn": "arn:aws:lambda:ap-northeast-1:588762728270:function:simple-lambda:4",
  "Runtime": "nodejs4.3",
  "Role": "arn:aws:iam::588762728270:role/simple-lambda-executor",
  "Handler": "lambda.handler",
  "CodeSize": 916,
  "Description": "",
  "Timeout": 3,
  "MemorySize": 128,
  "LastModified": "2016-12-16T01:56:46.710+0000",
  "CodeSha256": "BR8NbrwsKCTlkj+5BTR4TaygexFUXViOoYbd7xhEypU=",
  "Version": "4",
  "KMSKeyArn": null
}

スケジュールイベントの設定

CloudWatch Eventでスケジュールを元に定期的に実行する設定を行うことも可能です。
5分おきに実行する設定を作成するにはclaudia add-scheduled-eventを利用します。

$ claudia add-scheduled-event --event event.json \
  --name simple-lambda-scheduled-event \
  --schedule 'rate(5 minutes)' \
  --version test-version

スケジュールイベントの他に、S3イベント、SNSイベントを元に起動するよう設定することも可能です(claudia add-s3-event-source, claudia add-sns-event-source)。

API BuilderによりWeb APIを作成してみる

単独のLambda FunctionをデプロイしただけではClaudia.jsのメリットがまださほど実感できません。今度はAPI Buidlerを使ってAPI GatewayとLambdaによるWeb APIを作成してみたいと思います。

今回はDynamoDBを利用したCRUDアプリケーションを作成します。

事前テーブル作成

DynamoDBで以下のテーブルを作成しておきます。

$ aws dynamodb create-table --table-name wrestlers \
  --attribute-definitions AttributeName=id,AttributeType=N \
  --key-schema AttributeName=id,KeyType=HASH \
  --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
  --query TableDescription.TableArn --output text

プロレスラーを保存するwrestlersテーブルになります(この題材はどうなのかは置いといて)。

API Projectの作成

mkdir simple-api
cd simple-api

プロジェクトフォルダを作成し、必要なファイルを用意していきます。

package.json
{
  "name": "simple-api",
  "version": "1.0.0",
  "description": "",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.7.15",
    "claudia-api-builder": "^2.3.1"
  }
}

claudia-api-builderとDynamoDBを操作するためのaws-sdkをdependenciesに追加しています。

またLambdaからDynamoDBにアクセスするために、以下のIAM Policy用のjsonファイルを用意する必要があります。

policies/dynamodb.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": [
        "dynamodb:GetItem",
        "dyanmodb:Scan",
        "dynamodb:Query",
        "dynamodb:PutItem",
        "dynamodb:UpdateItem",
        "dynamodb:DeleteItem",
        "dynamodb:BatchGetItem",
        "dynamodb:BatchWriteItem"
      ],
      "Effect": "Allow",
      "Resource": "*"
    }
  ]
}

アプリのコードは以下となります。Express等でWebサーバを作るときと雰囲気が似ています。基本的なCRUD操作を行うAPIです。ベースは example-projects/dynamodb-example at master · claudiajs/example-projects を参考にています。

app.js
const ApiBuilder = require('claudia-api-builder')
const AWS = require('aws-sdk')
const dynamoDB = new AWS.DynamoDB.DocumentClient()
const api = new ApiBuilder()
const tableName = 'wrestlers'
module.exports = api

api.post('/wrestlers', function(request){
  const params = {
    TableName: tableName,
    Item: {
      id: request.body.id,
      name: request.body.name,
      finishingMove: request.body.finishingMove,
    }
  }
  return dynamoDB.put(params).promise();
}, { success: 201 })


api.get('/wrestlers/{id}', function(request){
  const id = parseInt(request.pathParams.id, 10)
  const params = {
    TableName: tableName,
    Key: { id: id },
  }
  return dynamoDB.get(params).promise().then(function(response){
    return response.Item;
  })
})

api.get('/wrestlers', function(request){
  const params = {
    TableName: tableName,
  }
  return dynamoDB.scan(params).promise().then(function(response){
    return response.Items;
  })
})

api.patch('/wrestlers/{id}', function(request){
  const id = parseInt(request.pathParams.id, 10)
  const params = {
    TableName: tableName,
    Key: { id: id },
    UpdateExpression: 'SET #f1 = :v1, #f2 = :v2',
    ExpressionAttributeNames: {
      '#f1': 'name',
      '#f2': 'finishingMove'
    },
    ExpressionAttributeValues: {
      ':v1': request.body.name,
      ':v2': request.body.finishingMove,
    }
  }
  return dynamoDB.update(params).promise()
})

api.delete('/wrestlers/{id}', function(request){
  const id = parseInt(request.pathParams.id, 10)
  const params = {
    TableName: tableName,
    Key: { id: id },
  }
  return dynamoDB.delete(params).promise().then(function(response){
    return response.Item
  })
})

各リソースのメソッド内では最後に以下のようなコードになっていますが、

return dynamoDB.update(params).promise()

DynamoDBのDocumentClientは.promise()によりPromiseオブジェクトを返すことができます。また、API BuilderではPromiseオブジェクトをそのまま戻り値にすることができます。

Getメソッドの場合はPromiseのなかでresponse.Itemを返していますが、こうするとそのオブジェクトの内容をレスポンスにすることができます。

  return dynamoDB.get(params).promise().then(function(response){
    return response.Item;
  })

APIのデプロイ

claudia createでデプロイを行います。
API Builderの場合は--api-moduleで対象のコードを指定します。また、DynamoDBを操作するためのIAMポリシーのJSONファイルが置かれたディレクトリを--policiesオプションで指定しています。

$ claudia create --region ap-northeast-1 --api-module app --policies policies
saving configuration
{
  "lambda": {
    "role": "simple-api-executor",
    "name": "simple-api",
    "region": "ap-northeast-1"
  },
  "api": {
    "id": "zpoqi40p34",
    "module": "app",
    "url": "https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest"
  }
}

APIのテスト

ではAPIを実際にテストしてみます。

レコードの作成

$ curl -H "Content-Type: application/json" -X POST \
  --data '{ "id": 1, "name": "内藤哲也", "finishingMove": "Destino" }' \
  https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers
$ curl -H "Content-Type: application/json" -X POST \
  --data '{ "id": 2, "name": "棚橋弘至", "finishingMove": "ハイフライフロー" }' \
  https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers
$ curl -H "Content-Type: application/json" -X POST \
  --data '{ "id": 3, "name": "オカダカズチカ", "finishingMove": "レインメーカー" }' \
  https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers

レコードの取得

ID指定での取得

$ curl -X GET https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers/1
{"name":"内藤哲也","id":1,"finishingMove":"Destino"}

一覧取得

$ curl -X GET https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers
[{"name":"オカダカズチカ","id":3,"finishingMove":"レインメーカー"},{"name":"棚橋弘至","id":2,"finishingMove":"ハイフライフロー"},{"name":"内藤哲也","id":1,"finishingMove":"Destino"}]

レコードの更新

$ # 更新
$ curl -H "Content-Type: application/json" -X PATCH \
  --data '{ "id": 1, "name": "内藤哲也", "finishingMove": "デスティーノ" }' \
  https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers/1
$ # 確認
$ curl -X GET https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers/1
{"name":"内藤哲也","id":1,"finishingMove":"デスティーノ"}

レコードの削除

curl -X DELETE https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers/3

レスポンスのステータスコードを404にしてみる

今のままだと、Getでレコードがなかった場合も200が返ってきてしまいます。そこで、Getを以下のように変更します。

api.get('/wrestlers/{id}', function(request){
  const id = parseInt(request.pathParams.id, 10)
  const params = {
    TableName: tableName,
    Key: { id: id },
  }
  return dynamoDB.get(params).promise().then(function(response){
    if (!response.Item) {
      throw new Error('not found')
    }
    return response.Item;
  })
}, { success: 200, error: 404 })

.get()メソッドの3つ目の引数に{ success: 200, error: 404 } を指定しています。処理中では、レコードが存在しなかったら例外をthrowしています。

この状態でレコードが存在しないリソースを指定してAPIを実行すると、

$ curl -I -X GET https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers/999
HTTP/1.1 404 Not Found
Content-Type: application/json
Content-Length: 28
Connection: keep-alive
...

404が返ってきます。

※内容に応じて400, 404といったようにステータスコードを動的に変えたい場合は、以下のサンプルのように ApiBuilder.ApiResponseオブジェクトを返せば行けそうです
https://github.com/claudiajs/example-projects/blob/master/web-api-custom-status-code/web.js

IAM認証を追加してみる

デフォルトでは特に認証はつきませんが、IAM認証を設定することが可能です。
試しにdelete処理にIAM認証を設定してみます。

api.delete('/wrestlers/{id}', function(request){
  const id = parseInt(request.pathParams.id, 10)
  const params = {
    TableName: tableName,
    Key: { id: id },
  }
  return dynamoDB.delete(params).promise().then(function(response){
    return response.Item
  })
}, { success: 200, error: 404, authorizationType: 'AWS_IAM' })

2つ目の引数のオプションでauthorizationType: 'AWS_IAM' }を指定しています。

試しにこの状態でDeleteしようとすると、

$ curl -I -X DELETE https://spzc7xv95a.execute-api.ap-northeast-1.amazonaws.com/latest/wrestlers/3

HTTP/1.1 403 Forbidden

403が返ってきます。

その他に独自の認証を設定することも可能です。
claudia-api-builder/api.md at master · claudiajs/claudia-api-builder

Interceptorを設定してみる

すべてのリクエストで共通に行いたい処理がある場合は、Interceptorを使うことで実現できます。

たとえば、毎回requestオブジェクトの内容をログ出力したい場合は以下のようにします。

api.intercept((request) => {
  console.log(
    'context: ', request.context,
    ' pathParams:', request.pathParams,
    ' queryString:', request.queryString,
    ' body: ', request.body
  )
  return request
})

Interceptorではリクエストパラメータをフィルタリング、変更したり、条件によって処理を次に渡さずにレスポンスを返してしまったりすることも可能です。

claudia-api-builder/api.md at master · claudiajs/claudia-api-builder

終わりに

APIを作成する際、個別にAPI Gatewayの設定を手動で行うのはかなり大変で苦痛な作業です。リソースの作成、メソッドの作成、リクエストパラメータの変換、レスポンスの変換、ステータスコードの設定等、すべき作業が山ほどあります。

API Builderを使うとそれらの設定を個別に行う必要がなく、Node.jsのコードから自動で設定してくれます。YAMLにリソースパスを記載したりする必要もありません。これはNode.jsに特化している大きなメリットだと思います。

Lambdaの開発でプログラミング言語にNode.jsを使用できる場合は、有力な選択肢となるツールの一つだと思います。

この投稿は Serverless(2) Advent Calendar 201616日目の記事です。