🧗♂️
API GatewayとDynamoDBで爆速なREST APIを構築してみた
ローカル環境
- macOS 13.0.1
- cdk 2.51.1
- node 16.15.1
設計
「記事」のリソースが必要と仮定してAPIを構築します。
API仕様
メソッド | エンドポイント | 概要 |
---|---|---|
GET | /posts | 記事一覧取得 |
GET | /posts/{id} | 記事取得 |
POST | /posts/{id} | 記事追加 |
PUT | /posts/{id} | 記事更新 |
DELETE | /posts/{id} | 記事削除 |
DB設計
PK | 項目名 | 型 |
---|---|---|
○ | id | String |
title | String | |
content | String | |
createAt | Number | |
updatedAt | Number |
AWS構成図
API Gatewayでは「統合リクエスト」「統合レスポンス」を定義します。
- 統合リクエスト・・・DynamoDBにどのような操作(CRUD)をするのか定義します。
- 統合レスポンス・・・クライアントが必要としているデータ形式に整形します。
実装
CDKはAWSリソースをデプロイするためのツールです。
本記事での詳細な説明は割愛します。
スタック
agw-integrate-ddb-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as iam from "aws-cdk-lib/aws-iam";
import reqGetPost from "../src/agw/reqTemplates/reqGetPost";
import resGetPost from "../src/agw/resTemplates/resGetPost";
import { HttpMethod } from "aws-cdk-lib/aws-events";
import reqListPost from "../src/agw/reqTemplates/reqListPost";
import resListPost from "../src/agw/resTemplates/resListPost";
import reqDeletePost from "../src/agw/reqTemplates/reqDeletePost";
import reqPutPost from "../src/agw/reqTemplates/reqPutPost";
import reqUpdatePost from "../src/agw/reqTemplates/reqUpdatePost";
import resDeletePost from "../src/agw/resTemplates/resDeletePost";
import resPutPost from "../src/agw/resTemplates/resPutPost";
import resUpdatePost from "../src/agw/resTemplates/resUpdatePost";
export class AgwIntegrateDdbStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// DynamoDBテーブル
const ddbTable = new dynamodb.Table(this, "PostTable", {
tableName: "Post",
partitionKey: {
name: "id",
type: dynamodb.AttributeType.STRING,
},
billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
// API Gateway
const restApi = new apigateway.RestApi(this, "RestApi", {
restApiName: "api",
cloudWatchRole: true,
deployOptions: {
loggingLevel: apigateway.MethodLoggingLevel.ERROR,
metricsEnabled: true,
dataTraceEnabled: true,
},
defaultCorsPreflightOptions: {
allowOrigins: apigateway.Cors.ALL_ORIGINS,
},
});
// API GatewayがDynamoDBにアクセスするためのIAMロール
const credentialsRole = new iam.Role(this, "AccessibleDdbRole", {
assumedBy: new iam.ServicePrincipal("apigateway.amazonaws.com"),
});
// DynamoDBへのCRUD権限を付与
ddbTable.grantReadWriteData(credentialsRole);
// /posts/{id}のエンドポイントを作成する
const posts = restApi.root.addResource("posts");
const post = posts.addResource("{id}");
// 【GET】/posts
this.addMethod(
posts,
credentialsRole,
HttpMethod.GET,
dynamodb.Operation.SCAN,
reqListPost,
resListPost
);
// 【GET】/posts/{id}
this.addMethod(
post,
credentialsRole,
HttpMethod.GET,
dynamodb.Operation.GET_ITEM,
reqGetPost,
resGetPost
);
// 【POST】/posts/{id}
this.addMethod(
post,
credentialsRole,
HttpMethod.POST,
dynamodb.Operation.PUT_ITEM,
reqPutPost,
resPutPost
);
// 【PUT】/posts/{id}
this.addMethod(
post,
credentialsRole,
HttpMethod.PUT,
dynamodb.Operation.UPDATE_ITEM,
reqUpdatePost,
resUpdatePost
);
// 【DELETE】/posts/{id}
this.addMethod(
post,
credentialsRole,
HttpMethod.DELETE,
dynamodb.Operation.DELETE_ITEM,
reqDeletePost,
resDeletePost
);
}
/**
* API GatewayにHTTP Methodを追加する
* @param resource メソッド追加対象のリソース
* @param credentialsRole DynamoDBアクセス用のIAMロール
* @param httpMethod 追加するHTTPメソッド
* @param action DynamoDBの操作タイプ
* @param requestTemplates 統合リクエストのテンプレート
* @param responseTemplates 統合レスポンスのテンプレート
*/
private addMethod(
resource: apigateway.Resource,
credentialsRole: iam.Role,
httpMethod: HttpMethod,
action: dynamodb.Operation,
requestTemplates: {
[contentType: string]: string;
},
responseTemplates: {
[contentType: string]: string;
}
) {
resource.addMethod(
httpMethod,
new apigateway.AwsIntegration({
service: "dynamodb",
action,
options: {
credentialsRole,
requestTemplates,
integrationResponses: [
{
statusCode: "200",
responseTemplates,
},
],
},
}),
{
methodResponses: [
{
statusCode: "200",
},
],
}
);
}
}
DynamoDBへのアクセスの最小権限を一行で設定している部分や、使いまわす冗長なコードはメソッド化しているところがポイントです。
統合テンプレート
テンプレートはVTLエンジンで動作しています。
JavaScriptのような親しみのある言語がサポートされて欲しいですね。
意外と世に出回っているリファレンス少なく、試行錯誤で実装してみたので参考にしてみてください。
【GET】/posts
reqListPost.ts
export default {
"application/json": `{
"TableName": "Post"
}`,
};
resListPost.ts
export default {
"application/json": `{
#set($body=$input.path('$'))
"data":[
#foreach($item in $body.Items)
{
"id": "$item.id.S",
"title": "$item.title.S",
"content": "$item.content.S",
"createdAt": "$item.createdAt.N",
"updatedAt": "$item.updatedAt.N"
}
#if($foreach.hasNext),#end
#end
]
}`,
};
【GET】/posts/{id}
reqGetPost.ts
export default {
"application/json": `{
"TableName": "Post",
"Key": {
"id": {
"S": "$input.params('id')"
}
}
}`,
};
resGetPost.ts
export default {
"application/json": `{
#set($body=$input.path('$'))
#if($body.Item.id.S!="")
"id": "$body.Item.id.S",
"title": "$body.Item.title.S",
"content": "$body.Item.content.S",
"createdAt": "$item.createdAt.N",
"updatedAt": "$item.updatedAt.N"
#end
}`,
};
【POST】/posts/{id}
reqPutPost.ts
export default {
"application/json": `{
"TableName": "Post",
"Item": {
"id": {
"S": "$input.params('id')"
},
"title": {
"S": $input.json('$.title')
},
"content": {
"S": $input.json('$.content')
},
"createdAt": {
"N": "$context.requestTimeEpoch"
},
"updatedAt": {
"N": "$context.requestTimeEpoch"
}
}
}`,
};
resPutPost.ts
export default {
"application/json": `{}`
}
【PUT】/posts/{id}
reqUpdatePost.ts
export default {
"application/json": `{
"TableName": "Post",
"Key": {
"id": {
"S": "$input.params('id')"
}
},
"AttributeUpdates": {
#set($body=$input.path('$'))
#if($body.title != "")
"title": {
"Action":"PUT",
"Value":{
"S": "$body.title"
}
},
#end
#if($body.content != "")
"content": {
"Action":"PUT",
"Value":{
"S": "$body.content"
}
},
#end
"updatedAt": {
"Action":"PUT",
"Value":{
"N": "$context.requestTimeEpoch"
}
}
}
}`,
};
resUpdatePost.ts
export default {
"application/json": `{}`
}
【DELETE】/posts/{id}
reqDeletePost.ts
export default {
"application/json": `{
"TableName": "Post",
"Key": {
"id": {
"S": "$input.params('id')"
}
}
}`,
};
resDeletePost.ts
export default {
"application/json": `{}`
}
まとめ
API GatewayとDynamoDBの間のLambdaをなくすことで、管理するリソースが減り、コストが削減され、パフォーマンスも向上します。
API統合が使える場面では積極的に採用していきたいですね。
ではまた👋
Discussion