Skip to content

Commit ef48d98

Browse files
authored
feat: only re-deploy db bootstrap if code or config parameters change (#225)
1 parent d725bb9 commit ef48d98

File tree

1 file changed

+82
-11
lines changed

1 file changed

+82
-11
lines changed

lib/database/index.ts

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ import {
1616
resolveLambdaCode,
1717
} from "../utils";
1818
import { PgBouncer } from "./PgBouncer";
19+
import * as crypto from "crypto";
20+
import * as fs from "fs";
21+
import * as path from "path";
1922

2023
const instanceSizes: Record<string, number> = require("./instance-memory.json");
2124

@@ -25,11 +28,49 @@ let defaultPgSTACCustomOptions: { [key: string]: any } = {
2528
};
2629

2730
function hasVpc(
28-
instance: rds.DatabaseInstance | rds.IDatabaseInstance
31+
instance: rds.DatabaseInstance | rds.IDatabaseInstance,
2932
): instance is rds.DatabaseInstance {
3033
return (instance as rds.DatabaseInstance).vpc !== undefined;
3134
}
3235

36+
/**
37+
* Computes a content-based hash for Lambda Docker build assets.
38+
*
39+
* This hash includes:
40+
* - Dockerfile content
41+
* - Handler code content
42+
* - Build arguments (PGSTAC_VERSION, PYTHON_VERSION)
43+
*
44+
* @param basePath - Base directory containing the bootstrapper_runtime folder
45+
* @param buildArgs - Docker build arguments that affect the Lambda
46+
* @returns SHA256 hash as hex string
47+
*/
48+
function computeLambdaCodeHash(
49+
basePath: string,
50+
buildArgs: { [key: string]: string },
51+
): string {
52+
const hash = crypto.createHash("sha256");
53+
54+
// Hash Dockerfile content
55+
const dockerfilePath = path.join(basePath, "bootstrapper_runtime/Dockerfile");
56+
const dockerfileContent = fs.readFileSync(dockerfilePath, "utf8");
57+
hash.update(`Dockerfile:${dockerfileContent}`);
58+
59+
// Hash handler code
60+
const handlerPath = path.join(basePath, "bootstrapper_runtime/handler.py");
61+
const handlerContent = fs.readFileSync(handlerPath, "utf8");
62+
hash.update(`handler:${handlerContent}`);
63+
64+
// Hash build arguments in sorted order for consistency
65+
const sortedArgs = Object.keys(buildArgs)
66+
.sort()
67+
.map((key) => `${key}=${buildArgs[key]}`)
68+
.join(",");
69+
hash.update(`buildArgs:${sortedArgs}`);
70+
71+
return hash.digest("hex");
72+
}
73+
3374
/**
3475
* An RDS instance with pgSTAC installed and PgBouncer connection pooling.
3576
*
@@ -89,7 +130,7 @@ export class PgStacDatabase extends Construct {
89130

90131
const defaultParameters = this.getParameters(
91132
props.instanceType?.toString() || "m5.large",
92-
props.parameters
133+
props.parameters,
93134
);
94135
const parameterGroup = new rds.ParameterGroup(this, "parameterGroup", {
95136
engine: props.engine,
@@ -118,19 +159,22 @@ export class PgStacDatabase extends Construct {
118159
const { code: userCode, ...otherLambdaOptions } =
119160
props.bootstrapperLambdaFunctionOptions || {};
120161

162+
// Store build args for hash computation
163+
const buildArgs = {
164+
PYTHON_VERSION: "3.12",
165+
PGSTAC_VERSION: this.pgstacVersion,
166+
};
167+
121168
const handler = new aws_lambda.Function(this, "lambda", {
122169
// defaults
123170
runtime: aws_lambda.Runtime.PYTHON_3_12,
124171
handler: "handler.handler",
125172
memorySize: 128,
126173
logRetention: aws_logs.RetentionDays.ONE_WEEK,
127-
timeout: Duration.minutes(2),
174+
timeout: Duration.minutes(15),
128175
code: resolveLambdaCode(userCode, __dirname, {
129176
file: "bootstrapper_runtime/Dockerfile",
130-
buildArgs: {
131-
PYTHON_VERSION: "3.12",
132-
PGSTAC_VERSION: this.pgstacVersion,
133-
},
177+
buildArgs: buildArgs,
134178
}),
135179
vpc: hasVpc(this.db) ? this.db.vpc : props.vpc,
136180
allowPublicSubnet: true,
@@ -181,10 +225,18 @@ export class PgStacDatabase extends Construct {
181225
// if props.lambdaFunctionOptions doesn't have 'code' defined, update pgstac_version (needed for default runtime)
182226
if (!userCode) {
183227
customResourceProperties["pgstac_version"] = this.pgstacVersion;
228+
229+
// Add content-based hash to ensure the Lambda gets re-executed only when code or config changes
230+
customResourceProperties["code_hash"] = computeLambdaCodeHash(
231+
__dirname,
232+
buildArgs,
233+
);
184234
}
185235

186-
// add timestamp to properties to ensure the Lambda gets re-executed on each deploy
187-
customResourceProperties["timestamp"] = new Date().toISOString();
236+
// force the bootstrap process to run by adding a timestamp which will ensure the custom resource executes the Lambda function
237+
if (props.forceBootstrap) {
238+
customResourceProperties["timestamp"] = new Date().toISOString();
239+
}
188240

189241
const bootstrapper = new CustomResource(this, "bootstrapper", {
190242
serviceToken: handler.functionArn,
@@ -197,7 +249,7 @@ export class PgStacDatabase extends Construct {
197249
instanceName: `${Stack.of(this).stackName}-pgbouncer`,
198250
instanceType: ec2.InstanceType.of(
199251
ec2.InstanceClass.T3,
200-
ec2.InstanceSize.MICRO
252+
ec2.InstanceSize.MICRO,
201253
),
202254
};
203255
const addPgbouncer = props.addPgbouncer ?? true;
@@ -240,7 +292,7 @@ export class PgStacDatabase extends Construct {
240292

241293
public getParameters(
242294
instanceType: string,
243-
parameters: PgStacDatabaseProps["parameters"]
295+
parameters: PgStacDatabaseProps["parameters"],
244296
): DatabaseParameters {
245297
// https://github.com/aws/aws-cli/issues/1279#issuecomment-909318236
246298
const memory_in_kb = instanceSizes[instanceType] * 1024;
@@ -337,6 +389,25 @@ export interface PgStacDatabaseProps extends rds.DatabaseInstanceProps {
337389
* @default - defined in the construct.
338390
*/
339391
readonly bootstrapperLambdaFunctionOptions?: CustomLambdaFunctionProps;
392+
393+
/**
394+
* Force redeployment of the database bootstrapper Lambda on every deploy.
395+
*
396+
* This is only applicable when using custom Lambda code via bootstrapperLambdaFunctionOptions.
397+
* When enabled, a timestamp will be added to the custom resource properties to ensure
398+
* the bootstrapper Lambda runs on every deployment.
399+
*
400+
* For the default Docker-based bootstrap code, this flag is ignored and a content-based
401+
* hash is used instead (which automatically triggers redeployment when code changes).
402+
*
403+
* **Alternative approach:** Instead of using this flag, you can trigger bootstrap by
404+
* modifying any property in `customResourceProperties` (e.g., increment `pgstac_version`
405+
* or add a `rebuild_trigger` property with a new value). This gives you more granular
406+
* control over when redeployment happens.
407+
*
408+
* @default false
409+
*/
410+
readonly forceBootstrap?: boolean;
340411
}
341412

342413
export interface DatabaseParameters {

0 commit comments

Comments
 (0)