Skip to content

Commit 94cbbeb

Browse files
authored
feat: add patching infrastructure for pgbouncer instance (#230)
1 parent baf82c6 commit 94cbbeb

File tree

4 files changed

+141
-15
lines changed

4 files changed

+141
-15
lines changed

lib/database/PatchManager.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import * as ssm from 'aws-cdk-lib/aws-ssm';
2+
import * as iam from 'aws-cdk-lib/aws-iam';
3+
import { Construct } from 'constructs';
4+
5+
export interface PatchManagerProps {
6+
/**
7+
* The EC2 instance ID to apply patches
8+
*/
9+
instanceId: string;
10+
11+
/**
12+
* Custom maintenance window
13+
*
14+
* @default - New maintenance window
15+
* schedule: Sundays at 07:00 UTC, duration: 3 hours, cutoff: 1 hour
16+
*/
17+
maintenanceWindow?: ssm.CfnMaintenanceWindow;
18+
}
19+
20+
export class PatchManager extends Construct {
21+
constructor(scope: Construct, id: string, props?: PatchManagerProps) {
22+
super(scope, id);
23+
24+
// IAM role used by the maintenance window
25+
const maintenanceRole = new iam.Role(this, 'MaintenanceWindowRole', {
26+
assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'),
27+
});
28+
29+
maintenanceRole.addToPolicy(
30+
new iam.PolicyStatement({
31+
actions: [
32+
'ssm:SendCommand',
33+
'ssm:ListCommands',
34+
'ssm:ListCommandInvocations',
35+
],
36+
resources: ['*'],
37+
}),
38+
);
39+
40+
// Maintenance Window
41+
const maintenanceWindow = props?.maintenanceWindow ?? new ssm.CfnMaintenanceWindow(
42+
this,
43+
'PatchMaintenanceWindow',
44+
{
45+
name: 'patch-maintenance-window',
46+
description: 'Weekly patching using AWS default patch baseline',
47+
schedule: 'cron(0 7 ? * SUN *)', // Sundays 07:00 UTC
48+
duration: 3,
49+
cutoff: 1,
50+
allowUnassociatedTargets: false,
51+
},
52+
);
53+
54+
// Target EC2 instance by Instance ID
55+
if (!props?.instanceId) {
56+
throw new Error('instanceId is required to create PatchManagerStack');
57+
}
58+
const target = new ssm.CfnMaintenanceWindowTarget(
59+
this,
60+
'PatchTarget',
61+
{
62+
windowId: maintenanceWindow.ref,
63+
resourceType: 'INSTANCE',
64+
targets: [
65+
{
66+
key: 'InstanceIds',
67+
values: [props.instanceId],
68+
},
69+
],
70+
},
71+
);
72+
73+
// Patch task (Install)
74+
new ssm.CfnMaintenanceWindowTask(this, 'PatchInstallTask', {
75+
windowId: maintenanceWindow.ref,
76+
taskArn: 'AWS-RunPatchBaseline',
77+
taskType: 'RUN_COMMAND',
78+
priority: 1,
79+
maxConcurrency: '1',
80+
maxErrors: '1',
81+
serviceRoleArn: maintenanceRole.roleArn,
82+
targets: [
83+
{
84+
key: 'WindowTargetIds',
85+
values: [target.ref],
86+
},
87+
],
88+
taskInvocationParameters: {
89+
maintenanceWindowRunCommandParameters: {
90+
parameters: {
91+
Operation: ['Install'],
92+
},
93+
},
94+
},
95+
});
96+
}
97+
}

lib/database/index.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
aws_ec2 as ec2,
99
aws_rds as rds,
1010
aws_secretsmanager as secretsmanager,
11+
aws_ssm as ssm,
1112
} from "aws-cdk-lib";
1213
import { Construct } from "constructs";
1314
import {
@@ -16,6 +17,7 @@ import {
1617
resolveLambdaCode,
1718
} from "../utils";
1819
import { PgBouncer } from "./PgBouncer";
20+
import { PatchManager } from "./PatchManager";
1921
import * as crypto from "crypto";
2022
import * as fs from "fs";
2123
import * as path from "path";
@@ -124,6 +126,7 @@ export class PgStacDatabase extends Construct {
124126
public readonly securityGroup?: ec2.SecurityGroup;
125127
public readonly secretBootstrapper?: CustomResource;
126128
public readonly pgbouncerHealthCheck?: CustomResource;
129+
public readonly pgbouncerInstanceId?: string;
127130

128131
constructor(scope: Construct, id: string, props: PgStacDatabaseProps) {
129132
super(scope, id);
@@ -281,11 +284,21 @@ export class PgStacDatabase extends Construct {
281284

282285
this._pgBouncerServer.node.addDependency(bootstrapper);
283286

287+
// Patching infrastructure for PgBouncer instance
288+
const addPatchManager = props.addPatchManager ?? true;
289+
if (addPatchManager) {
290+
new PatchManager(this, "PatchManager", {
291+
instanceId: this._pgBouncerServer.instance.instanceId,
292+
maintenanceWindow: props.maintenanceWindow,
293+
});
294+
}
295+
284296
this.pgstacSecret = this._pgBouncerServer.pgbouncerSecret;
285297
this.connectionTarget = this._pgBouncerServer.instance;
286298
this.securityGroup = this._pgBouncerServer.securityGroup;
287299
this.secretBootstrapper = this._pgBouncerServer.secretUpdateComplete;
288300
this.pgbouncerHealthCheck = this._pgBouncerServer.healthCheck;
301+
this.pgbouncerInstanceId = this._pgBouncerServer.instance.instanceId;
289302
} else {
290303
this.connectionTarget = this.db;
291304
}
@@ -374,6 +387,21 @@ export interface PgStacDatabaseProps extends rds.DatabaseInstanceProps {
374387
*/
375388
readonly pgbouncerInstanceProps?: ec2.InstanceProps | any;
376389

390+
/**
391+
* Add patching system using AWS SSM for pgbouncer instance maintenance
392+
* `addPgbouncer` must be true for this to have an effect
393+
*
394+
* @default true
395+
*/
396+
readonly addPatchManager?: boolean;
397+
398+
/**
399+
* Custom maintenance window for patching
400+
*
401+
* @default - A new maintenance window will be created, defined in construct
402+
*/
403+
readonly maintenanceWindow?: ssm.CfnMaintenanceWindow;
404+
377405
/**
378406
* Lambda function Custom Resource properties. A custom resource property is going to be created
379407
* to trigger the boostrapping lambda function. This parameter allows the user to specify additional properties

package-lock.json

Lines changed: 15 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"@semantic-release/changelog": "^6.0.3",
4444
"@semantic-release/git": "^10.0.1",
4545
"@types/node": "^24.9.1",
46-
"aws-cdk-lib": "^2.220.0",
46+
"aws-cdk-lib": "2.220.0",
4747
"constructs": "10.4.2",
4848
"jsii": "5.9.10",
4949
"jsii-docgen": "10.11.0",

0 commit comments

Comments
 (0)