はじめに
こんにちは!
本記事は先日公開した本記事は先日公開した【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第2回 WAF適用編の続きとなります。
前回はWAFを適用し、特定のIPアドレスにのみAPI Gatewayへのアクセスを許可するようにしました。
第3回となる今回は、Lambda関数からDynamoDBにアクセスするAPIのロジック部分を実装したいと思います。
過去の記事はこちら
- 【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第1回(全3回)
- 【サーバーレス初心者向け】Serverless Framework + SwaggerでWeb APIを作る!第2回 WAF適用編(全3回)
今回作るもの(再掲)
今回は以下のアーキテクト図のようなWeb APIバックエンドを作っていきます。
API GatewayでクライアントからのAPIリクエストを受信し、該当するLambda関数を呼び出し、必要に応じてDynamoDBからのデータ読み出しおよび書き込みを行います。さらに、WAFを適用することでセキュアにします。
APIとしては、ID・名前・身長・体重・年齢の情報を持つPersonモデルを登録・取得・更新・削除するAPIを作りたいと思います。
- GET dev/slsTestApp/v1/api/person/{personId}
- POST dev/slsTestApp/v1/api/person
- PUT dev/slsTestApp/v1/api/person/{personId}
- DELETE dev/slsTestApp/v1/api/person/{personId}
実装手順
今回はGET APIを例に以下の手順で実装していきたいと思います。
- DynamoDBのテンプレート実装→デプロイ
- IAM Roleのテンプレート実装→デプロイ
- Lambda関数の実装→デプロイ
DynamoDBのテンプレート実装→デプロイ
DynamoDBのテンプレートは以下のようになります。
Resources:
SlsTestAppPersonTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: SlsTestAppPersonTable-${self:provider.stage}
AttributeDefinitions:
- AttributeName: personId
AttributeType: S
- AttributeName: age
AttributeType: S
KeySchema:
- AttributeName: personId
KeyType: HASH
- AttributeName: age
KeyType: RANGE
BillingMode: PAY_PER_REQUEST
レコードのIDとなるPersonId
をパーティションキー、age
をソートキーにしたテーブルとします。BillingMode
で課金モードを指定できます。今回はテストアプリのためアクセスはほとんどしないため、使った分だけ課金されるPAY_PER_REQUEST
を指定しています。もしたくさんのアクセスが予想されるシステムへ流用するならあらかじめキャパシティユニットを指定するPROVISIONED
のほうが安く済む場合もあります。
両モードの料金比較はクラスメソッドさんに記事がありますのでこちらを参照してみてください。
DynamoDB料金比較
https://dev.classmethod.jp/articles/reinvent2018-compare-dynamodb-on-demand-price-with-provisioned-price/
その他の記載方法は以下を参照してください。
DynamoDB CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html
テンプレートが完成したら、一度デプロイします。DBはステートフルリソースなので、Lambda関数とはスタックを分けたほうが良いです。そのため、前回の記事で取り上げたWAFのデプロイのように、dynamodb
フォルダを作り、その下に先ほどのdynamodb.yml
とserverless.yml
を作成します。serverless.yml
は以下のような感じです。
# Welcome to Serverless!
#
# 中略
service: slsTestAppDynamoDb
# app and org for use with dashboard.serverless.com
#app: your-app-name
# 中略
provider:
name: aws
runtime: nodejs12.x
# you can overwrite defaults here
stage: dev
region: ap-northeast-1
# 中略
# you can add packaging information here
package:
# include:
# - include-me.js
# - include-me-dir/**
exclude:
- templates/**
#functions:
# functionsは記載しない
# you can add CloudFormation resource templates here
resources:
- ${file(./templates/dynamodb.yml)}
ここまでできたら、dynamodb
フォルダまでコマンドプロンプトを起動し、デプロイコマンドを実行します。
sls deploy -v
IAM Roleのテンプレート実装→デプロイ
Lambda関数からDynamoDBにアクセスするにはIAM Roleでアクセス権限をつける必要がありますので、そのテンプレートを作成します。
テンプレートは以下のようになります。
Resources:
GetPersonRole:
Type: AWS::IAM::Role
Properties:
RoleName: "GetPersonRole-${self:provider.stage}"
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Principal:
Service: "lambda.amazonaws.com"
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Path: "/"
Policies:
- PolicyName: "GetPersonPolicy-${self:provider.stage}"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action:
- dynamodb:Query
Resource:
- "arn:aws:dynamodb:${self:provider.region}:${self:provider.environment.ACCOUNT}:table/SlsTestAppPersonTable-${self:provider.stage}"
- "arn:aws:dynamodb:${self:provider.region}:${self:provider.environment.ACCOUNT}:table/SlsTestAppPersonTable-${self:provider.stage}/index/*"
まず、ManagedPolicy
として、AWSLambdaBasicExecutionRole
を付けます。これをつけることで実行結果がCloudWatch Logsに出力されるようになり、デバッグがしやすくなります。
GET APIではバックエンドでDynamoのQueryを使用する想定なので、dynamodb:Query
を許可する記載にしています。Resource
にはアクセスしたいDyanmoDBのArnを指定しますが、Queryやセカンダリーインデックスを使ったアクセスの許可の場合には{DynamoDBのArn}/index/*
も追加する必要があることに注意してください。
それではデプロイしていきます。Lambda関数定義が記載されたsrerverless.ymlのresources
にiam.yml
へのパスを指定し、デプロイコマンドを実行してください。
なお、IAM RoleもDynamoDBやWAFと同様にスタックを分けて作成しても構いません。
Lambda関数の実装→デプロイ
ここまで来たらロジックを実装していきます。
まずはDynamoDBにアクセスする関数を実装していきます。
コードは以下の通りです。
"use strict";
module.exports = class PersonTable {
constructor(serviceClient) {
this.client = serviceClient;
}
getPerson(personId) {
const params = {
TableName: process.env.PERSON_TABLE_NAME,
KeyConditionExpression: "#hash = :personId",
ExpressionAttributeNames: {
"#hash": "personId",
},
ExpressionAttributeValues: {
":personId": personId,
},
};
return new Promise((resolve, reject) => {
this.client.query(params, (err, data) => {
if (err) {
console.log(err);
reject(err);
} else {
console.log("getPerson Success!");
resolve(data);
}
});
});
}
};
少し特殊かもしれませんが、DynamoDBにアクセスする関数をまとめたクラスPersonTable
の中にAPI別にメソッドを実装しています。メソッドを使用するときは呼び出し元でDynamoDBクライアントを作りそれをコンストラクターに渡してあげます。
次に、API Gatewayから呼び出されるLambda関数を実装していきます。
第1回で作成したindex.jsを以下のように変更します。
"use strict";
var AWS = require("aws-sdk");
AWS.config.update({ region: process.env.region });
//ここでDynamoDBのクライアントを作る
var docClient = new AWS.DynamoDB.DocumentClient({ apiVersion: "2012-08-10" });
//先ほどのPersonTableをインポート
var PersonTable = require("../../aws/personTable");
var Validator = require("../../util/validator");
var Formatter = require("../../util/formatter");
module.exports.handler = async (event, context, callback) => {
//ここでクライアントを渡す
const personTable = new PersonTable(docClient);
const validator = new Validator();
const formatter = new Formatter();
try {
//DynamoDBからPersonモデルを取得する
const res = await personTable.getPerson(event.pathParameters.personId);
if (validator.checkDyanmoQueryResultEmpty(res)) {
const errorModel = {
errorCode: "STA00001",
errorMessage: "Not Found",
};
callback(null, {
statusCode: 404,
body: JSON.stringify({
errorModel,
}),
});
}
callback(null, {
statusCode: 200,
body: JSON.stringify(formatter.getPersonFormatter(res)),
});
} catch (err) {
console.log("getPersonTable-index error");
}
};
コメント残している箇所がDynamoDBアクセスに関係する箇所です。
event
にパスパラメータ(personId)が入っているのでそれを取得してPersonTable.getPerson
に渡しています。
validator
やformatter
は詳しくは述べませんが、それぞれDynamoDBアクセスの結果をバリデーションするクラス/フォーマットするクラスです。
ここまでできたらデプロイしてください。
動作確認
では早速動作確認していきましょう。GET APIなので、DynamoDBに手動でレコードを登録しておきます。例えば以下のような内容です。
{
"personId": "aaa",
"age": "28",
"height": "178",
"name": "tarou",
"weight": "70"
}
GET APIをcurlで実行し、先ほどのjsonが返ってくれば成功です!
> curl GET https://XXX.execute-api.ap-northeast-1.amazonaws.com/dev/slsTestApp/v1/api/person/aaa
{"personId": "aaa","age": "28","height": "178","name": "tarou","weight": "70"}
おわりに
最後までご覧いただきありがとうございました。
ここまでのサンプルコード一式や、紹介しきれなかったその他のPOST/PUT/DELETEのAPI実装、単体テストなどなど以下のGitHubに置いておきましたので、参考していただければ幸いです。