Slide 1

Slide 1 text

DynamoDBの"Replacement"時にデータ が消されないように Custom Resource Provider Frameworkで カスタムリソース作ってみた件 クラスメソッド株式会社 リテールアプリ共創部 マッハチーム morimorikochan

Slide 2

Slide 2 text

名前 morimorikochan 所属 クラスメソッド株式会社    リテールアプリ共創部    マッハチーム🚀 趣味 Switchのゲーム X @marooon88 ⼀⾔ 中古7万の椅⼦でQoL爆上 ⾃⼰紹介

Slide 3

Slide 3 text

1. 結論 2. 問題提起 3. Custom Resources 4. Custom Resource Provider Framework 5. 実装した 6. まとめ 󰞹内容

Slide 4

Slide 4 text

● CDKでDynamoDBのテーブル置換時、テーブルのデータが全件削除され て困っていた ● CDKのcustomresources.Provider(通称Custom Resource Provider Framework)を使って新テーブルへのコピーを⾃動で⾏えるようにした ● 処理時間も早く、CIへの影響も少ない ● 課題 ○ Lambda関数のIAMロールがちょっとガバってしまう ○ レースコンディション発⽣する 📚結論

Slide 5

Slide 5 text

󰞵 DynamoDBのPrimaryKey/SortKeyこんな感じやろか 󰞵 とりあえず cdk deploy して開発開始や! ⸻⸻数⽇後⸻⸻ 󰞵 PrimaryKey/SortKey間違ってたわ、直して cdk deploy っと...   ほなさいなら...(データ全削除) 󰞵 うわあぁぁぁぁぁぁ動作確認⽤のデータが消えたあぁぁぁぁぁぁ 🤨こんなことないですか

Slide 6

Slide 6 text

🤨こんなことないですか

Slide 7

Slide 7 text

🤨こんなことないですか

Slide 8

Slide 8 text

📚こんなことないですか 🤔 CDKの⼒でなんとかならん?

Slide 9

Slide 9 text

📚こんなことないですか 💡 Custom Resource Provider Framework

Slide 10

Slide 10 text

カスタムリソースを使⽤すると、CloudFormation テンプレートにカスタムプロビジョニン グロジックを記述し、スタックを作成、更新 (カスタムリソースを変更した場合)、または削 除するたびに CloudFormation にそのロジックを実⾏させることができます。これは、プロ ビジョニング要件に、CloudFormation の組み込みリソースタイプでは表現できない複雑な ロジックやワークフローが含まれる場合に役⽴ちます。 カスタムリソースプロバイダーの定義⽅法 ● SNS ○ 1. メッセージが送られる ○ 2. SNSのバックエンド側で受信し処理 ○ 3. バックエンド側がS3にファイルをPUTし応答する ● Lambda ○ 1. Lambda関数が実⾏される ○ 2. Lambda関数内で処理 ○ 3. Lambda関数内でS3にファイルをPUTし応答する 🤔Custom Resourcesとは?

Slide 11

Slide 11 text

CDKでの定義⽅法は4種類 1. sns.Topic ○ 前ページの処理を⾏うSNSを定義 2. lambda.Function ○ 前ページの処理を⾏うLambdaを定義 3. core.CustomResourceProvider ○ 2を使いやすくした便利ラッパー(?) ○ エラーハンドリングや応答をLambda関数の結果から⾃動で⾏ってくれる ○ 15分のタイムアウト制限がある ○ アプリケーション実装者には推奨されていない 4. customresources.Provider(Custom Resource Provider Framework) ○ 3をさらに使いやすくした便利ラッパー(?) ○ 別途“処理が終わったかどうかを判定する”ためのLambda関数 (isCompleteHandler)を定義可能なので、実質15分のタイムアウトがない >aws-cdk-lib module · AWS CDK 🤔CDKでどうやってCustom Resourcesを定義する?

Slide 12

Slide 12 text

🤔カスタムリソースプロバイダーにCRPFを使った場合 1. CloudFormation実⾏中にLambda関数が呼び出され、 同時にカスタムリソースが作成中/更新中/削除中になる 2. Lambda関数が応答する 3. カスタムリソースが完了状態になる ※isCompleteHandlerを利⽤しない場合

Slide 13

Slide 13 text

● DynamoDBがリソース置換されたことをカスタムリソースが検知し ● Lambda関数を実⾏させる ● Lambda関数内では以下の処理を⾏う ○ 1. 旧テーブルからScanして全アイテムを取得 ○ 2. 新テーブルに全アイテムをBatchWriteItem この⽅法の他にも、AWSのAPI経由で旧テーブルからS3へコピーしそれを新 テーブルへコピーする⽅法もあったが⾒送り ● 処理時間がかかる ● DMSがややこしそう(DMSエアプ) 💡今回の課題へ適⽤すると...

Slide 14

Slide 14 text

const lambdaFunction = new nodejsLambda.NodejsFunction(/*略*/) lambdaFunction.addToRolePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["dynamodb:Scan"], resources: ["*"], }) ); props.ddbTable.grantWriteData(lambdaFunction); const provider = new cr.Provider(this, "CustomResourceProvider", { onEventHandler: lambdaFunction, }); new cdk.CustomResource(this, "CustomResource", { serviceToken: provider.serviceToken, properties: { tableName: props.ddbTable.tableName, } as ResourceProperties, }); 󰳕利⽤イメージ(CDK側)

Slide 15

Slide 15 text

const lambdaFunction = new nodejsLambda.NodejsFunction(/*略*/) lambdaFunction.addToRolePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ["dynamodb:Scan"], resources: ["*"], }) ); props.ddbTable.grantWriteData(lambdaFunction); const provider = new cr.Provider(this, "CustomResourceProvider", { onEventHandler: lambdaFunction, }); new cdk.CustomResource(this, "CustomResource", { serviceToken: provider.serviceToken, properties: { tableName: props.ddbTable.tableName, } as ResourceProperties, }); 󰳕利⽤イメージ(CDK側) const originalTable = new dynamodb.Table(/* 略 */) new DynamoDbTableItemsRestorer(this,'OriginalTableItemsRestorer',{ ddbTable: originalTable }) 利⽤例

Slide 16

Slide 16 text

テーブルが置き換わる場合(=物理IDが変化した場合)のみ Lambdaを実⾏したい 🤔Lambda関数どう実装する? テーブルの変化 旧テーブルから新テーブルへ コピーさせるか テーブル作成時 ❌ テーブル更新時 (置換なし) ❌ テーブル更新時 (置換あり) 󰢐 テーブル削除時 ❌

Slide 17

Slide 17 text

/** Lambdaのエントリーポイント */ export const handler: CdkCustomResourceHandler = async (event) => { logger.info("リソースが変化しました ", {event}); switch (event.RequestType) { case "Update": await copyAllItems({ fromTableName: event.OldResourceProperties.tableName, newTableName: event.ResourceProperties.tableName, }); return { PhysicalResourceId: event.ResourceProperties.tableName }; case "Create": case "Delete": return {}; } }; 󰳕利⽤イメージ(Lambda側)

Slide 18

Slide 18 text

/** Lambdaのエントリーポイント */ export const handler: CdkCustomResourceHandler = async (event) => { logger.info("リソースが変化しました ", {event}); switch (event.RequestType) { case "Update": await copyAllItems({ fromTableName: event.OldResourceProperties.tableName, newTableName: event.ResourceProperties.tableName, }); return { PhysicalResourceId: event.ResourceProperties.tableName }; case "Create": case "Delete": return {}; } }; 󰳕利⽤イメージ(Lambda側) new cdk.CustomResource(this, "CustomResource", { serviceToken: provider.serviceToken, properties: { tableName: props.ddbTable.tableName, } as ResourceProperties, });

Slide 19

Slide 19 text

前回からPhysicalResourceId(物理ID)変化させた場合 ● CloudFormationが”カスタムリソースが置換された”と認識する ○ もしロールバックが発⽣した場合はCloudFormationがDeleteを実⾏する 前回から変化させなかった場合 ● CloudFormationが”カスタムリソースが更新された”と認識する ○ もしロールバックが発⽣した場合はCloudFormationが パラメータを逆にしてUpdateを実⾏し、元の状態に戻そうとする 今回の要件では、ロールバック時に処理をさせる必要はないのでコードがシ ンプルになるよう”PhysicalResourceIdを変化させる” 󰞲Update時に返却する物理IDについて

Slide 20

Slide 20 text

const copyAllItems = async (props: { fromTableName: string, newTableName: string, } ) => { logger.info("データをコピーします ", {fromTableName: props.fromTableName, newTableName: props.newTableName}); const oldTableReadableStream = new DynamoDBReadableStream(ddbClient, props.fromTableName); const newTableWritableStream = new DynamoDBWritableStream(ddbClient, props.newTableName); await pipeline(oldTableReadableStream, newTableWritableStream) if (newTableWritableStream.errorChunks.length > 0) { logger.error("コピーに失敗したアイテムがあります ", {errorChunks: newTableWritableStream.errorChunks}) throw new Error(`${newTableWritableStream.errorChunks.length}件のデータをコピーでき ませんでした `) } } 󰳕利⽤イメージ(Lambda側)

Slide 21

Slide 21 text

💪実⾏してみた テーブルの置換

Slide 22

Slide 22 text

💪実⾏してみた カスタムリソース(Lambda) のポリシー変更

Slide 23

Slide 23 text

💪実⾏してみた カスタムリソースが実⾏されてる 物理IDが変化しているので、 カスタムリソースが置換されたと 認識されてる

Slide 24

Slide 24 text

💪実⾏してみた Lambda関数の処理時間 ● Item数が少ない場合はだいたい10秒とか ● これぐらいならCIへの影響が少ない Lambda関数のIAMロールがちょっとガバってしまう ● CDK上で旧テーブル名を保持していないため ● SSM使えばできそうですけど 複雑にしたくない レースコンディション発⽣する(当然) ● コピーの途中に旧テーブルにPutItemされると、そのデータは新テーブルにコピーされ ない可能性がある ● 本番環境で利⽤する際は、メンテナンスモードなど検討必要 lambdaFunction.addToRolePolicy( new iam.PolicyStatement({ resources: ["*"], }) );

Slide 25

Slide 25 text

📚まとめ ● CDKでDynamoDBのテーブル置換時、テーブルのデータが全件削除され て困っていた ● CDKのcustomresources.Provider(通称Custom Resource Provider Framework)を使って新テーブルへのコピーを⾃動で⾏えるようにした ● 処理時間も早く、CIへの影響も少ない ● 課題 ○ Lambda関数のIAMロールがちょっとガバってしまう ○ レースコンディション発⽣する

Slide 26

Slide 26 text

📚資料 作成したコード ● https://github.com/diggymo/ddb-table-item-restorer 参考資料 ● aws-cdk-lib module · AWS CDK ○ https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib-readme.htm l#custom-resources ● CDK で Custom resources を作成する|伊藤忠テクノソリューションズ ○ https://www.ctc-g.co.jp/solutions/cloud/column/article/84.html

Slide 27

Slide 27 text

ありがとうございました 🫠おわり