Skip to content

Commit 31018b2

Browse files
committed
Enhance infrastructure setup by adding RDS PostgreSQL instance with auto-scaling storage, security groups, and monitoring alarms. Introduce common tags for resource management and update dependencies to include Pulumi Random package.
1 parent b069fe2 commit 31018b2

File tree

3 files changed

+328
-7
lines changed

3 files changed

+328
-7
lines changed

apps/infra/index.ts

Lines changed: 311 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,33 @@
11
import * as aws from "@pulumi/aws";
22
import * as awsx from "@pulumi/awsx";
33
import * as pulumi from "@pulumi/pulumi";
4+
import * as random from "@pulumi/random";
45

5-
// VPC with public subnets for ALB and private subnets for containers
6-
const vpc = new awsx.ec2.Vpc("pathfinder-vpc");
6+
// ==========================================
7+
// CONFIGURATION & SHARED SETTINGS
8+
// ==========================================
9+
10+
// Common tags for all resources
11+
const commonTags = {
12+
Project: "pathfinder",
13+
Environment: pulumi.getStack(), // Will be "dev", "staging", "production" etc.
14+
ManagedBy: "pulumi",
15+
Owner: "platform-team",
16+
CreatedDate: new Date().toISOString().split("T")[0], // YYYY-MM-DD
17+
};
18+
19+
// ==========================================
20+
// NETWORKING LAYER
21+
// VPC, Subnets, Security Groups
22+
// ==========================================
723

8-
const cluster = new aws.ecs.Cluster("pathfinder-cluster");
24+
// VPC with public subnets for ALB and private subnets for containers
25+
const vpc = new awsx.ec2.Vpc("pathfinder-vpc", {
26+
tags: {
27+
...commonTags,
28+
Name: "pathfinder-vpc",
29+
},
30+
});
931

1032
// Security Groups
1133
// ALB: Allow HTTP from internet
@@ -27,6 +49,11 @@ const albSecurityGroup = new aws.ec2.SecurityGroup("pathfinder-alb-sg", {
2749
cidrBlocks: ["0.0.0.0/0"],
2850
},
2951
],
52+
tags: {
53+
...commonTags,
54+
Name: "pathfinder-alb-sg",
55+
Type: "load-balancer",
56+
},
3057
});
3158

3259
// Service: Only accept traffic from ALB (not directly from internet)
@@ -50,13 +77,174 @@ const serviceSecurityGroup = new aws.ec2.SecurityGroup(
5077
cidrBlocks: ["0.0.0.0/0"],
5178
},
5279
],
80+
tags: {
81+
...commonTags,
82+
Name: "pathfinder-service-sg",
83+
Type: "ecs-service",
84+
},
85+
}
86+
);
87+
88+
// Database Security Group - only allow access from the service
89+
const dbSecurityGroup = new aws.ec2.SecurityGroup("pathfinder-db-sg", {
90+
vpcId: vpc.vpcId,
91+
ingress: [
92+
{
93+
protocol: "tcp",
94+
fromPort: 5432,
95+
toPort: 5432,
96+
securityGroups: [serviceSecurityGroup.id], // Only allow from ECS service
97+
},
98+
],
99+
egress: [
100+
{
101+
protocol: "-1",
102+
fromPort: 0,
103+
toPort: 0,
104+
cidrBlocks: ["0.0.0.0/0"],
105+
},
106+
],
107+
tags: {
108+
...commonTags,
109+
Name: "pathfinder-db-sg",
110+
Type: "database",
111+
},
112+
});
113+
114+
// ==========================================
115+
// DATABASE LAYER
116+
// RDS PostgreSQL with auto-scaling storage
117+
// ==========================================
118+
119+
// RDS Subnet Group - database should be in private subnets
120+
const dbSubnetGroup = new aws.rds.SubnetGroup("pathfinder-db-subnet-group", {
121+
subnetIds: vpc.privateSubnetIds,
122+
tags: {
123+
...commonTags,
124+
Name: "pathfinder-db-subnet-group",
125+
},
126+
});
127+
128+
// Generate a secure random password for the database
129+
const dbPassword = new random.RandomPassword("pathfinder-db-password", {
130+
length: 32,
131+
special: true,
132+
overrideSpecial: "!@#$%&*()-_=+[]{}<>:?", // Avoid problematic characters in connection strings
133+
});
134+
135+
// RDS PostgreSQL Instance
136+
const db = new aws.rds.Instance("pathfinder-db", {
137+
engine: "postgres",
138+
engineVersion: "15.4",
139+
instanceClass: "db.t3.small", // Upgraded from micro: 2 vCPU, 2 GB RAM
140+
allocatedStorage: 50, // 50 GB (up from 20 GB)
141+
maxAllocatedStorage: 1000, // Auto-scale storage up to 1 TB as needed
142+
storageType: "gp3",
143+
storageEncrypted: true, // Enable encryption at rest
144+
dbName: "pathfinder",
145+
username: "pathfinder_admin",
146+
password: dbPassword.result, // Use the generated secure password
147+
vpcSecurityGroupIds: [dbSecurityGroup.id],
148+
dbSubnetGroupName: dbSubnetGroup.name,
149+
skipFinalSnapshot: true, // For dev - set to false in production
150+
deletionProtection: false, // For dev - set to true in production
151+
backupRetentionPeriod: 7, // Keep backups for 7 days
152+
backupWindow: "03:00-04:00", // 3-4 AM UTC
153+
maintenanceWindow: "sun:04:00-sun:05:00", // Sunday 4-5 AM UTC
154+
enabledCloudwatchLogsExports: ["postgresql"], // Export logs to CloudWatch
155+
tags: {
156+
...commonTags,
157+
Name: "pathfinder-db",
158+
Engine: "postgresql",
159+
Tier: "database",
160+
},
161+
});
162+
163+
// ==========================================
164+
// MONITORING & OBSERVABILITY
165+
// CloudWatch Alarms for proactive monitoring
166+
// ==========================================
167+
168+
// Database Monitoring
169+
const dbHighCPUAlarm = new aws.cloudwatch.MetricAlarm(
170+
"pathfinder-db-high-cpu",
171+
{
172+
comparisonOperator: "GreaterThanThreshold",
173+
evaluationPeriods: 2,
174+
metricName: "CPUUtilization",
175+
namespace: "AWS/RDS",
176+
period: 300, // 5 minutes
177+
statistic: "Average",
178+
threshold: 80,
179+
alarmDescription: "Triggers when database CPU exceeds 80% for 10 minutes",
180+
dimensions: {
181+
DBInstanceIdentifier: db.id,
182+
},
183+
tags: {
184+
...commonTags,
185+
Name: "pathfinder-db-high-cpu-alarm",
186+
Type: "monitoring",
187+
},
188+
}
189+
);
190+
191+
const dbHighConnectionsAlarm = new aws.cloudwatch.MetricAlarm(
192+
"pathfinder-db-high-connections",
193+
{
194+
comparisonOperator: "GreaterThanThreshold",
195+
evaluationPeriods: 2,
196+
metricName: "DatabaseConnections",
197+
namespace: "AWS/RDS",
198+
period: 300, // 5 minutes
199+
statistic: "Average",
200+
threshold: 80, // db.t3.small has max ~100 connections, so 80 is 80%
201+
alarmDescription: "Triggers when database connections exceed 80",
202+
dimensions: {
203+
DBInstanceIdentifier: db.id,
204+
},
205+
tags: {
206+
...commonTags,
207+
Name: "pathfinder-db-high-connections-alarm",
208+
Type: "monitoring",
209+
},
210+
}
211+
);
212+
213+
const dbLowStorageAlarm = new aws.cloudwatch.MetricAlarm(
214+
"pathfinder-db-low-storage",
215+
{
216+
comparisonOperator: "LessThanThreshold",
217+
evaluationPeriods: 1,
218+
metricName: "FreeStorageSpace",
219+
namespace: "AWS/RDS",
220+
period: 300, // 5 minutes
221+
statistic: "Average",
222+
threshold: 5 * 1024 * 1024 * 1024, // 5 GB in bytes
223+
alarmDescription: "Triggers when free storage drops below 5 GB",
224+
dimensions: {
225+
DBInstanceIdentifier: db.id,
226+
},
227+
tags: {
228+
...commonTags,
229+
Name: "pathfinder-db-low-storage-alarm",
230+
Type: "monitoring",
231+
},
53232
}
54233
);
55234

56-
// Load Balancer with health checks
235+
// ==========================================
236+
// LOAD BALANCING & TRAFFIC MANAGEMENT
237+
// Application Load Balancer with health checks
238+
// ==========================================
239+
57240
const lb = new awsx.lb.ApplicationLoadBalancer("pathfinder-lb", {
58241
subnetIds: vpc.publicSubnetIds,
59242
securityGroups: [albSecurityGroup.id],
243+
tags: {
244+
...commonTags,
245+
Name: "pathfinder-lb",
246+
Type: "application-load-balancer",
247+
},
60248
defaultTargetGroup: {
61249
port: 3000,
62250
protocol: "HTTP",
@@ -73,9 +261,27 @@ const lb = new awsx.lb.ApplicationLoadBalancer("pathfinder-lb", {
73261
},
74262
});
75263

264+
// ==========================================
265+
// CONTAINER INFRASTRUCTURE
266+
// ECR, ECS Service, IAM Roles, Logging
267+
// ==========================================
268+
269+
// ECS Cluster
270+
const cluster = new aws.ecs.Cluster("pathfinder-cluster", {
271+
tags: {
272+
...commonTags,
273+
Name: "pathfinder-cluster",
274+
},
275+
});
276+
76277
// Logging
77278
const logGroup = new aws.cloudwatch.LogGroup("pathfinder-logs", {
78279
retentionInDays: 7,
280+
tags: {
281+
...commonTags,
282+
Name: "pathfinder-logs",
283+
Type: "application-logs",
284+
},
79285
});
80286

81287
// ECS needs this role to pull images and write logs
@@ -102,7 +308,12 @@ new aws.iam.RolePolicyAttachment("pathfinder-execution-role-policy", {
102308

103309
// ECR for storing Docker images
104310
const repo = new awsx.ecr.Repository("pathfinder-repo", {
105-
forceDelete: true,
311+
forceDelete: true, // Allow deletion even if images exist
312+
tags: {
313+
...commonTags,
314+
Name: "pathfinder-repo",
315+
Type: "container-registry",
316+
},
106317
});
107318

108319
// This builds AND pushes your Docker image to ECR automatically:
@@ -145,6 +356,10 @@ const service = new awsx.ecs.FargateService("pathfinder-service", {
145356
name: "PORT",
146357
value: "3000",
147358
},
359+
{
360+
name: "DATABASE_URL",
361+
value: pulumi.interpolate`postgresql://${db.username}:${db.password}@${db.endpoint}/${db.dbName}`,
362+
},
148363
],
149364
portMappings: [
150365
{
@@ -164,7 +379,11 @@ const service = new awsx.ecs.FargateService("pathfinder-service", {
164379
},
165380
});
166381

167-
// Auto-scaling: 2-10 tasks based on CPU
382+
// ==========================================
383+
// AUTO-SCALING CONFIGURATION
384+
// Automatically scale ECS tasks based on CPU
385+
// ==========================================
386+
168387
const scaling = new aws.appautoscaling.Target("pathfinder-scaling", {
169388
maxCapacity: 10,
170389
minCapacity: 2,
@@ -189,4 +408,90 @@ const scalingPolicy = new aws.appautoscaling.Policy(
189408
}
190409
);
191410

411+
// ==========================================
412+
// ADDITIONAL MONITORING
413+
// ECS Service and Load Balancer health checks
414+
// ==========================================
415+
416+
const ecsHighCPUAlarm = new aws.cloudwatch.MetricAlarm(
417+
"pathfinder-ecs-high-cpu",
418+
{
419+
comparisonOperator: "GreaterThanThreshold",
420+
evaluationPeriods: 2,
421+
metricName: "CPUUtilization",
422+
namespace: "AWS/ECS",
423+
period: 300, // 5 minutes
424+
statistic: "Average",
425+
threshold: 80,
426+
alarmDescription:
427+
"Triggers when ECS service CPU exceeds 80% (may trigger auto-scaling)",
428+
dimensions: {
429+
ClusterName: cluster.name,
430+
ServiceName: service.service.name,
431+
},
432+
tags: {
433+
...commonTags,
434+
Name: "pathfinder-ecs-high-cpu-alarm",
435+
Type: "monitoring",
436+
},
437+
}
438+
);
439+
440+
const albUnhealthyHostsAlarm = new aws.cloudwatch.MetricAlarm(
441+
"pathfinder-alb-unhealthy-hosts",
442+
{
443+
comparisonOperator: "GreaterThanThreshold",
444+
evaluationPeriods: 2,
445+
metricName: "UnHealthyHostCount",
446+
namespace: "AWS/ApplicationELB",
447+
period: 60, // 1 minute
448+
statistic: "Average",
449+
threshold: 0,
450+
alarmDescription: "Triggers when ALB has unhealthy targets",
451+
dimensions: {
452+
LoadBalancer: lb.loadBalancer.arnSuffix,
453+
TargetGroup: lb.defaultTargetGroup.arnSuffix,
454+
},
455+
treatMissingData: "notBreaching",
456+
tags: {
457+
...commonTags,
458+
Name: "pathfinder-alb-unhealthy-alarm",
459+
Type: "monitoring",
460+
},
461+
}
462+
);
463+
464+
const albResponseTimeAlarm = new aws.cloudwatch.MetricAlarm(
465+
"pathfinder-alb-high-response-time",
466+
{
467+
comparisonOperator: "GreaterThanThreshold",
468+
evaluationPeriods: 3,
469+
metricName: "TargetResponseTime",
470+
namespace: "AWS/ApplicationELB",
471+
period: 60, // 1 minute
472+
statistic: "Average",
473+
threshold: 2, // 2 seconds
474+
alarmDescription: "Triggers when response time exceeds 2 seconds",
475+
dimensions: {
476+
LoadBalancer: lb.loadBalancer.arnSuffix,
477+
},
478+
treatMissingData: "notBreaching",
479+
tags: {
480+
...commonTags,
481+
Name: "pathfinder-alb-response-time-alarm",
482+
Type: "monitoring",
483+
},
484+
}
485+
);
486+
487+
// ==========================================
488+
// STACK OUTPUTS
489+
// Values accessible after deployment
490+
// ==========================================
491+
192492
export const url = lb.loadBalancer.dnsName.apply((dns) => `http://${dns}`);
493+
export const dbEndpoint = db.endpoint;
494+
export const dbConnectionString = pulumi.interpolate`postgresql://${db.username}:${db.password}@${db.endpoint}/${db.dbName}`;
495+
// Export password as a secret - only visible via CLI with --show-secrets flag
496+
export const dbPasswordSecret = pulumi.secret(dbPassword.result);
497+
export const dbUsername = db.username;

apps/infra/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
"dependencies": {
99
"@pulumi/aws": "^6.83.0",
1010
"@pulumi/awsx": "^2.22.0",
11-
"@pulumi/pulumi": "^3.181.0"
11+
"@pulumi/pulumi": "^3.181.0",
12+
"@pulumi/random": "^4.18.2"
1213
}
1314
}

0 commit comments

Comments
 (0)