infra/seed.yaml declares the table, a role allowing only dynamodb:BatchWriteItem on it, and the function booknest-seeder (nodejs24.x) with Code: seeder/, a directory that aws cloudformation package zips and uploads (Nested Stacks), holding the handler and a copy of db/seed.json. The custom resource passes the table's name, cuts the one-hour timeout to two minutes, and exposes the handler's Count:
SeedBooks:
Type: Custom::BookSeed
Properties:
ServiceToken: !GetAtt SeederFunction.Arn
ServiceTimeout: 120
TableName: !Ref BooksTable
Outputs:
SeededCount:
Value: !GetAtt SeedBooks.Countimport { readFile } from "node:fs/promises";
import { DynamoDBClient, BatchWriteItemCommand } from "@aws-sdk/client-dynamodb";
import { marshall } from "@aws-sdk/util-dynamodb";
const db = new DynamoDBClient({});
async function respond(event, Status, Data = {}, Reason = "see CloudWatch Logs") {
const { StackId, RequestId, LogicalResourceId, ResourceProperties } = event;
const PhysicalResourceId = `seed-${ResourceProperties.TableName}`;
const body = JSON.stringify({ Status, Reason, PhysicalResourceId, StackId, RequestId,
LogicalResourceId, Data });
await fetch(event.ResponseURL, { method: "PUT", body, headers: { "content-type": "" } });
}
export const handler = async (event) => {
try {
const { books } = JSON.parse(await readFile("./seed.json", "utf8"));
const toRequest = event.RequestType === "Delete"
? (b) => ({ DeleteRequest: { Key: marshall({ id: b.id }) } })
: (b) => ({ PutRequest: { Item: marshall(b) } });
const table = event.ResourceProperties.TableName;
const RequestItems = { [table]: books.map(toRequest) };
await db.send(new BatchWriteItemCommand({ RequestItems }));
await respond(event, "SUCCESS", { Count: books.length });
} catch (err) {
await respond(event, "FAILED", {}, `${err.name}: ${err.message}`);
}
};The runtime includes the AWS 24 SDK for JavaScript v3, so no node_modules are packaged. One BatchWriteItem writes the books, or removes them on Delete, and respond() is the whole protocol: a JSON body PUT to the ResponseURL, with the empty content type that the presigned URL was signed for. AWS's cfn-response module does the same, but only inline ZipFile code can load it.
cd ~/v5-ch7/booknest/infra
aws s3 mb s3://booknest-templates
aws cloudformation package --template-file seed.yaml --s3-bucket booknest-templates \
--output-template-file seed.packaged.yaml > /dev/null
aws cloudformation deploy --stack-name booknest-seed --template-file seed.packaged.yaml \
--capabilities CAPABILITY_IAM
aws cloudformation describe-stacks --stack-name booknest-seed --output text \
--query 'Stacks[0].[StackStatus,Outputs[0].OutputValue]'make_bucket: booknest-templates Uploading to d7a7926bb4f4b470957474784b01efa3 1542 / 1542.0 (100.00%) ... Successfully created/updated stack - booknest-seed CREATE_COMPLETE unknown
The stack is CREATE_COMPLETE, yet SeededCount is unknown: the handler never ran. Custom Resources on LocalStack explains why; first, drive the handler the way CloudFormation 24 would.