11import * as aws from "@pulumi/aws" ;
22import * as awsx from "@pulumi/awsx" ;
33import * 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+
57240const 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
77278const 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
104310const 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+
168387const 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+
192492export 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 ;
0 commit comments