@@ -25,6 +25,9 @@ export interface PgBouncerConfigProps {
2525 maxUserConnections ?: number ;
2626}
2727
28+ const DEFAULT_AMI_SSM_PARAMETER =
29+ "/aws/service/canonical/ubuntu/server/noble/stable/20260218/amd64/hvm/ebs-gp3/ami-id" ;
30+
2831export interface PgBouncerProps {
2932 /**
3033 * VPC to deploy PgBouncer into
@@ -62,10 +65,24 @@ export interface PgBouncerProps {
6265 instanceProps ?: Partial < ec2 . InstanceProps > ;
6366
6467 /**
65- * Optional reference to the database bootstrapper CustomResource.
66- * When provided, the health check will re-trigger if the database setup changes.
68+ * SSM parameter path for the PgBouncer EC2 instance machine image (AMI).
69+ *
70+ * Defaults to the latest Ubuntu Noble AMI. For stable deployments where
71+ * EC2 replacement should only happen on explicit intent (not on Canonical
72+ * AMI releases), pin this to a specific date-versioned path:
73+ * /aws/service/canonical/ubuntu/server/noble/stable/YYYYMMDD.X/amd64/hvm/ebs-gp3/ami-id
74+ *
75+ * To list available date-versioned paths in your region:
76+ * aws ssm get-parameters-by-path --path "/aws/service/canonical/ubuntu/server/noble/stable/" --recursive --query "Parameters[?ends_with(Name, 'amd64/hvm/ebs-gp3/ami-id')].Name"
77+ *
78+ * See: https://documentation.ubuntu.com/aws/aws-how-to/instances/find-ubuntu-images/
79+ *
80+ * With addPatchManager: true (default), SSM Patch Manager handles OS
81+ * security updates without requiring instance replacement.
82+ *
83+ * @default /aws/service/canonical/ubuntu/server/noble/stable/20260218/amd64/hvm/ebs-gp3/ami-id
6784 */
68- databaseBootstrapper ?: CustomResource ;
85+ readonly machineImageSsmParameter ?: string ;
6986}
7087
7188export class PgBouncer extends Construct {
@@ -81,7 +98,7 @@ export class PgBouncer extends Construct {
8198 // so we perform this calculation.
8299
83100 private getDefaultPgbouncerConfig (
84- dbMaxConnections : number
101+ dbMaxConnections : number ,
85102 ) : Required < PgBouncerConfigProps > {
86103 // maxDbConnections (and maxUserConnections) are the only settings that need
87104 // to be responsive to the database size/max_connections setting
@@ -103,7 +120,7 @@ export class PgBouncer extends Construct {
103120 // Set defaults for optional props
104121
105122 const defaultPgbouncerConfig = this . getDefaultPgbouncerConfig (
106- props . dbMaxConnections
123+ props . dbMaxConnections ,
107124 ) ;
108125
109126 // Merge provided config with defaults
@@ -119,10 +136,10 @@ export class PgBouncer extends Construct {
119136 assumedBy : new iam . ServicePrincipal ( "ec2.amazonaws.com" ) ,
120137 managedPolicies : [
121138 iam . ManagedPolicy . fromAwsManagedPolicyName (
122- "AmazonSSMManagedInstanceCore"
139+ "AmazonSSMManagedInstanceCore" ,
123140 ) ,
124141 iam . ManagedPolicy . fromAwsManagedPolicyName (
125- "CloudWatchAgentServerPolicy"
142+ "CloudWatchAgentServerPolicy" ,
126143 ) ,
127144 ] ,
128145 } ) ;
@@ -132,7 +149,7 @@ export class PgBouncer extends Construct {
132149 new iam . PolicyStatement ( {
133150 actions : [ "secretsmanager:GetSecretValue" ] ,
134151 resources : [ props . database . secret . secretArn ] ,
135- } )
152+ } ) ,
136153 ) ;
137154
138155 // Create a security group and allow connections from the Lambda IP ranges for this region
@@ -147,16 +164,16 @@ export class PgBouncer extends Construct {
147164 instanceName : "pgbouncer" ,
148165 instanceType : ec2 . InstanceType . of (
149166 ec2 . InstanceClass . T3 ,
150- ec2 . InstanceSize . MICRO
167+ ec2 . InstanceSize . MICRO ,
151168 ) ,
152169 vpcSubnets : {
153170 subnetType : props . usePublicSubnet
154171 ? ec2 . SubnetType . PUBLIC
155172 : ec2 . SubnetType . PRIVATE_WITH_EGRESS ,
156173 } ,
157174 machineImage : ec2 . MachineImage . fromSsmParameter (
158- "/aws/service/canonical/ubuntu/server/noble/stable/current/amd64/hvm/ebs-gp3/ami-id" ,
159- { os : ec2 . OperatingSystemType . LINUX }
175+ props . machineImageSsmParameter ?? DEFAULT_AMI_SSM_PARAMETER ,
176+ { os : ec2 . OperatingSystemType . LINUX } ,
160177 ) ,
161178 blockDevices : [
162179 {
@@ -185,7 +202,7 @@ export class PgBouncer extends Construct {
185202 props . database . connections . allowFrom (
186203 this . instance ,
187204 ec2 . Port . tcp ( 5432 ) ,
188- "Allow PgBouncer to connect to RDS"
205+ "Allow PgBouncer to connect to RDS" ,
189206 ) ;
190207
191208 // Create a new secret for pgbouncer connection credentials
@@ -205,7 +222,7 @@ export class PgBouncer extends Construct {
205222 runtime : lambda . Runtime . NODEJS_LATEST ,
206223 handler : "index.handler" ,
207224 code : lambda . Code . fromAsset (
208- path . join ( __dirname , "lambda/pgbouncer-secret-updater" )
225+ path . join ( __dirname , "lambda/pgbouncer-secret-updater" ) ,
209226 ) ,
210227 environment : {
211228 SOURCE_SECRET_ARN : props . database . secret . secretArn ,
@@ -226,7 +243,7 @@ export class PgBouncer extends Construct {
226243 ? this . instance . instancePublicIp
227244 : this . instance . instancePrivateIp ,
228245 } ,
229- }
246+ } ,
230247 ) ;
231248
232249 // Add health check custom resource
@@ -238,10 +255,10 @@ export class PgBouncer extends Construct {
238255 handler : "index.handler" ,
239256 timeout : Duration . minutes ( 10 ) ,
240257 code : lambda . Code . fromAsset (
241- path . join ( __dirname , "lambda/pgbouncer-health-check" )
258+ path . join ( __dirname , "lambda/pgbouncer-health-check" ) ,
242259 ) ,
243260 description : "PgBouncer health check function" ,
244- }
261+ } ,
245262 ) ;
246263
247264 // Grant SSM permissions for health check
@@ -254,17 +271,13 @@ export class PgBouncer extends Construct {
254271 "ssm:ListCommandInvocations" ,
255272 ] ,
256273 resources : [ "*" ] ,
257- } )
274+ } ) ,
258275 ) ;
259276
260277 this . healthCheck = new CustomResource ( this , "PgBouncerHealthCheck" , {
261278 serviceToken : healthCheckFunction . functionArn ,
262279 properties : {
263280 InstanceId : this . instance . instanceId ,
264- // Reference the database bootstrapper to re-trigger on database changes
265- ...( props . databaseBootstrapper && {
266- DatabaseBootstrapperRef : props . databaseBootstrapper . ref ,
267- } ) ,
268281 } ,
269282 } ) ;
270283
@@ -275,7 +288,7 @@ export class PgBouncer extends Construct {
275288
276289 private loadUserDataScript (
277290 pgBouncerConfig : Required < NonNullable < PgBouncerProps [ "pgBouncerConfig" ] > > ,
278- database : { secret : secretsmanager . ISecret }
291+ database : { secret : secretsmanager . ISecret } ,
279292 ) : ec2 . UserData {
280293 const userDataScript = ec2 . UserData . forLinux ( ) ;
281294
@@ -292,7 +305,9 @@ export class PgBouncer extends Construct {
292305 pgBouncerConfig . reservePoolTimeout +
293306 '"' ,
294307 'export MAX_DB_CONNECTIONS="' + pgBouncerConfig . maxDbConnections + '"' ,
295- 'export MAX_USER_CONNECTIONS="' + pgBouncerConfig . maxUserConnections + '"'
308+ 'export MAX_USER_CONNECTIONS="' +
309+ pgBouncerConfig . maxUserConnections +
310+ '"' ,
296311 ) ;
297312
298313 // Load the startup script
0 commit comments