@@ -16,6 +16,9 @@ import {
1616 resolveLambdaCode ,
1717} from "../utils" ;
1818import { PgBouncer } from "./PgBouncer" ;
19+ import * as crypto from "crypto" ;
20+ import * as fs from "fs" ;
21+ import * as path from "path" ;
1922
2023const instanceSizes : Record < string , number > = require ( "./instance-memory.json" ) ;
2124
@@ -25,11 +28,49 @@ let defaultPgSTACCustomOptions: { [key: string]: any } = {
2528} ;
2629
2730function 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
342413export interface DatabaseParameters {
0 commit comments