こんにちは、高崎@アノテーション です。
はじめに
(API Gateway⇔)Lambda⇔DynamoDB というサーバーレスの定番構成において、DynamoDB 上に動的な名前のテーブルを新規作成して使わないといけない局面が出てきました。
最終的には Lambda からテーブルを作らないように設計から見直したのですが、一時的に Lambda から DynamoDB のテーブルを作成する実装を調べた際、ネットに例文がなく色々と勉強になったのでブログ化します。
環境について
「はじめに」にて記載した通りサーバーレス定番構成ですが、TypeScript の cdk を使ってテンプレートを作成してデプロイする方式の構成です。
DynamoDB のテーブル名は Lambda 実行中に動的に生成される文字列を元にする必要があったのですが、後述の CreateTable の権限付与において範囲を絞れるよう「dydb_」というプレフィックスをテーブル名に付けて定義することにします。
構築
テンプレートの構築
考え方としては以下です。
- Lambda を定義する際にタイムアウトを1分設定にしておく(理由は後述)
- テンプレートからは DynamoDB の ARN を使って定義する
- Lambda ハンドラへは DynamoDB の Read/Write だけではなく DynamoDB のテーブル作成の権限も付与する
以下のような感じです。
※実装に必要な箇所のみを抜粋しています。
import * as cdk from "aws-cdk-lib";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import { Construct } from "constructs";
import { ScopedAws } from "aws-cdk-lib";
const stringTablePrefix = "dydb_";
export class TestStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
// Lambda 定義の際、timeout は 1 分設定にする
const lambdaTest = new lambda.Function(this, "LambdaTest", {
runtime: lambda.Runtime.NODEJS_18_X,
handler: "index.handler",
code: lambda.Code.fromAsset("Lambdaソースのディレクトリ"),
timeout: Duration.seconds(60),
environment: {
TABLE_NAME_PREFIX: stringTablePrefix,
},
});
// DynamoDB への ARN から Table インスタンスを生成
const { region, accountId } = new ScopedAws(scope);
const stringTableArn = `arn:aws:dynamodb:${region}:${accountId}:table/${stringTablePrefix}*`;
cost tableDynamoDB = dynamodb.Table.fromTableArn(scope, "StaticDynamoDB", stringTableArn);
// 生成したインスタンスを使って dynamodb::CreateTable を許可
tableDynamoDB.grant(lambdaTest.handler, "dynamodb::CreateTable");
// 同じく、Read/Write 権限を許可
tableDynamoDB.grantReadWriteData(lambdaTest.handler);
};
Lambda の構築
Lambda 側はテーブルを作ってからすぐ使えることを想定して下記のように実装します。
- CreateTableCommand を発行
- DescribeTableCommand を作ったテーブルに対して発行
- Describe の戻り値である TableStatus が ACTIVE になるまで待つ
テーブルの状態が ACTIVE になるのに(筆者の環境では約 10 秒弱)時間がかかるので、cdk の Lambda を定義する際、タイムアウト値を予め多めに設定する必要があります。
作成する関数を抜き出して記載します。
import { DynamoDBClient, CreateTableCommand, DescribeTableCommand } from "@aws-sdk/client-dynamodb";
export const sleep = async (ms: number) => {
return new Promise((resolve) => setTimeout(resolve, ms));
}
const stringPrefix: string = process.env.TABLE_NAME_PREFIX | "dydb_";
export async function createUserTable(stringName: string): Promise {
const stringTable: string = `${stringPrefix}${stringName}`;
const clientDB = new DynamoDBClient({ region: process.env.AWS_REGION });
const command = new CreateTableCommand({
TableName: stringTable,
BillingMode: "PAY_PER_REQUEST",
KeySchema: [
/* 適宜、キーを定義する */
],
AttributeDefinitions: [
/* キーの属性を定義する */
],
});
try {
const resCreate = await clientDB.send(command);
// 完全に出来きるまで待つ
// タイムアウト値は 500 msec × 60 の 30 秒とする
var numRetry = 60;
const commandDesc = new DescribeTableCommand({
"TableName": stringTable,
});
while (numRetry > 0) {
// 500 msec ウェイト
await sleep(500);
// DescribeTable を発行
const resDescribe = await clientDB.send(commandDesc);
if (resDescribe.Table?.TableStatus === "ACTIVE") {
break;
}
numRetry = numRetry - 1;
}
if (numRetry === 0) {
// リトライオーバーはエラー throw する
throw new Error("Create Wait Timeout");
}
return 0;
} catch (error) {
console.error("createUserTable : ", error);
}
return -1;
}
テーブルのキーについてはそれぞれ適宜実装してください。
引数のstringName
へ随意のテキストを設定し、関数の実行が成功して以降dydb_引数のテキスト
の名前のテーブルが作成され使用できます。
おわりに
今回は DynamoDB のテーブルを作成する Lambda を定番構成に盛り込んだ実装を記事にいたしました。
本来 DynamoDB のテーブルを Lambda から動的に作成する事態がないことが理想だと思いますので、冒頭でも申した通り「窮余の策として一時的にテーブルを作る場合」の想定になりますがご参考になれば幸いです。
アノテーション株式会社について
アノテーション株式会社は、クラスメソッド社のグループ企業として「オペレーション・エクセレンス」を担える企業を目指してチャレンジを続けています。
「らしく働く、らしく生きる」のスローガンを掲げ、様々な背景をもつ多様なメンバーが自由度の高い働き方を通してお客様へサービスを提供し続けてきました。
現在当社では一緒に会社を盛り上げていただけるメンバーを募集中です。
少しでもご興味あれば、アノテーション株式会社WEBサイト をご覧ください。