import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns'; import * as rds from 'aws-cdk-lib/aws-rds'; import * as ecr from 'aws-cdk-lib/aws-ecr'; import * as logs from 'aws-cdk-lib/aws-logs'; import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; import * as acm from 'aws-cdk-lib/aws-certificatemanager'; import * as route53 from 'aws-cdk-lib/aws-route53'; import { Construct } from 'constructs'; import * as dotenv from 'dotenv'; export interface BackendStackProps extends cdk.StackProps { vpcId?: string; databaseName?: string; certificateArn?: string; } export class BackendStack extends cdk.Stack { public readonly apiUrl: string; public readonly databaseEndpoint: string; public readonly vpc: ec2.IVpc; constructor(scope: Construct, id: string, props?: BackendStackProps) { super(scope, id, props); // Load environment variables from .env file dotenv.config({ path: '.env' }); // VPC - either use existing or create new this.vpc = props?.vpcId ? ec2.Vpc.fromLookup(this, 'ExistingVpc', { vpcId: props.vpcId }) : new ec2.Vpc(this, 'SyncrowVpc', { maxAzs: 2, natGateways: 1, subnetConfiguration: [ { cidrMask: 24, name: 'public', subnetType: ec2.SubnetType.PUBLIC, }, { cidrMask: 24, name: 'private', subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }, ], }); // Security Groups const dbSecurityGroup = new ec2.SecurityGroup(this, 'DatabaseSecurityGroup', { vpc: this.vpc, description: 'Security group for RDS PostgreSQL', allowAllOutbound: false, }); const ecsSecurityGroup = new ec2.SecurityGroup(this, 'EcsSecurityGroup', { vpc: this.vpc, description: 'Security group for ECS Fargate service', allowAllOutbound: true, }); const albSecurityGroup = new ec2.SecurityGroup(this, 'AlbSecurityGroup', { vpc: this.vpc, description: 'Security group for Application Load Balancer', allowAllOutbound: true, }); // Allow ALB to connect to ECS ecsSecurityGroup.addIngressRule( albSecurityGroup, ec2.Port.tcp(3000), 'Allow ALB to connect to ECS service' ); // Allow ECS to connect to RDS dbSecurityGroup.addIngressRule( ecsSecurityGroup, ec2.Port.tcp(5432), 'Allow ECS to connect to PostgreSQL' ); // Temporary access for admin IP dbSecurityGroup.addIngressRule( ec2.Peer.ipv4('216.126.231.231/32'), ec2.Port.tcp(5432), 'Temporary access from admin IP' ); // Allow HTTP/HTTPS traffic to ALB albSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(80), 'Allow HTTP traffic' ); albSecurityGroup.addIngressRule( ec2.Peer.anyIpv4(), ec2.Port.tcp(443), 'Allow HTTPS traffic' ); const dbCluster = rds.DatabaseCluster.fromDatabaseClusterAttributes(this, 'SyncrowDatabase', { clusterIdentifier: 'syncrow-backend', instanceIdentifiers: ['syncrowdatabase-instance-1'], engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_16_6, }), port: 5432, securityGroups: [ ec2.SecurityGroup.fromSecurityGroupId(this, 'ImportedDbSecurityGroup', 'sg-07e163f588b2bac25') ], clusterEndpointAddress: 'syncrowdatabase.cluster-criskv1sdkq4.me-central-1.rds.amazonaws.com', }); // Import the existing database secret separately const dbSecret = rds.DatabaseSecret.fromSecretCompleteArn(this, 'ImportedDbSecret', 'arn:aws:secretsmanager:me-central-1:482311766496:secret:rds!cluster-43ec14cd-9301-43e2-aa79-d330a429a126-v0JDQN' ); // ECR Repository for Docker images - import existing repository const ecrRepository = ecr.Repository.fromRepositoryName(this, 'SyncrowBackendRepo', 'syncrow-backend'); // Output the correct ECR URI for this region new cdk.CfnOutput(this, 'EcrRepositoryUriRegional', { value: ecrRepository.repositoryUri, description: `ECR Repository URI in region ${this.region}`, exportName: `${this.stackName}-EcrRepositoryUriRegional`, }); // ECS Cluster const cluster = new ecs.Cluster(this, 'SyncrowCluster', { vpc: this.vpc, clusterName: 'syncrow-backend-cluster', }); // CloudWatch Log Group const logGroup = new logs.LogGroup(this, 'SyncrowBackendLogs', { logGroupName: '/ecs/syncrow-backend', retention: logs.RetentionDays.ONE_WEEK, removalPolicy: cdk.RemovalPolicy.DESTROY, }); // Use existing wildcard certificate or create new one const apiCertificate = props?.certificateArn ? acm.Certificate.fromCertificateArn(this, 'ApiCertificate', props.certificateArn) : new acm.Certificate(this, 'ApiCertificate', { domainName: 'api.syncrow.me', validation: acm.CertificateValidation.fromDns(), }); // ECS Fargate Service with Application Load Balancer const fargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(this, 'SyncrowBackendService', { cluster, memoryLimitMiB: 1024, cpu: 512, desiredCount: 1, domainName: 'api.syncrow.me', domainZone: route53.HostedZone.fromLookup(this, 'SyncrowZone', { domainName: 'syncrow.me', }), certificate: apiCertificate, protocol: elbv2.ApplicationProtocol.HTTPS, redirectHTTP: true, taskImageOptions: { image: ecs.ContainerImage.fromEcrRepository(ecrRepository, 'latest'), containerPort: 3000, enableLogging: true, environment: { // App settings NODE_ENV: process.env.NODE_ENV || 'production', PORT: process.env.PORT || '3000', BASE_URL: process.env.BASE_URL || '', // Database connection (CDK provides these automatically) AZURE_POSTGRESQL_HOST: dbCluster.clusterEndpoint.hostname, AZURE_POSTGRESQL_PORT: '5432', AZURE_POSTGRESQL_DATABASE: props?.databaseName || 'syncrow', AZURE_POSTGRESQL_USER: 'postgres', AZURE_POSTGRESQL_SSL: process.env.AZURE_POSTGRESQL_SSL || 'false', AZURE_POSTGRESQL_SYNC: process.env.AZURE_POSTGRESQL_SYNC || 'false', // JWT Configuration - CRITICAL: These must be set JWT_SECRET: process.env.JWT_SECRET || 'syncrow-jwt-secret-key-2025-production-environment-very-secure-random-string', JWT_SECRET_REFRESH: process.env.JWT_SECRET_REFRESH || 'syncrow-refresh-secret-key-2025-production-environment-different-secure-string', JWT_EXPIRE_TIME: process.env.JWT_EXPIRE_TIME || '1h', JWT_EXPIRE_TIME_REFRESH: process.env.JWT_EXPIRE_TIME_REFRESH || '7d', // Firebase Configuration FIREBASE_API_KEY: process.env.FIREBASE_API_KEY || '', FIREBASE_AUTH_DOMAIN: process.env.FIREBASE_AUTH_DOMAIN || '', FIREBASE_PROJECT_ID: process.env.FIREBASE_PROJECT_ID || '', FIREBASE_STORAGE_BUCKET: process.env.FIREBASE_STORAGE_BUCKET || '', FIREBASE_MESSAGING_SENDER_ID: process.env.FIREBASE_MESSAGING_SENDER_ID || '', FIREBASE_APP_ID: process.env.FIREBASE_APP_ID || '', FIREBASE_MEASUREMENT_ID: process.env.FIREBASE_MEASUREMENT_ID || '', FIREBASE_DATABASE_URL: process.env.FIREBASE_DATABASE_URL || '', // Tuya IoT Configuration TUYA_EU_URL: process.env.TUYA_EU_URL || 'https://openapi.tuyaeu.com', TUYA_ACCESS_ID: process.env.TUYA_ACCESS_ID || '', TUYA_ACCESS_KEY: process.env.TUYA_ACCESS_KEY || '', TRUN_ON_TUYA_SOCKET: process.env.TRUN_ON_TUYA_SOCKET || '', // Email Configuration SMTP_HOST: process.env.SMTP_HOST || '', SMTP_PORT: process.env.SMTP_PORT || '587', SMTP_SECURE: process.env.SMTP_SECURE || 'true', SMTP_USER: process.env.SMTP_USER || '', SMTP_PASSWORD: process.env.SMTP_PASSWORD || '', // Mailtrap Configuration MAILTRAP_API_TOKEN: process.env.MAILTRAP_API_TOKEN || '', MAILTRAP_INVITATION_TEMPLATE_UUID: process.env.MAILTRAP_INVITATION_TEMPLATE_UUID || '', MAILTRAP_EDIT_USER_TEMPLATE_UUID: process.env.MAILTRAP_EDIT_USER_TEMPLATE_UUID || '', MAILTRAP_DISABLE_TEMPLATE_UUID: process.env.MAILTRAP_DISABLE_TEMPLATE_UUID || '', MAILTRAP_ENABLE_TEMPLATE_UUID: process.env.MAILTRAP_ENABLE_TEMPLATE_UUID || '', MAILTRAP_DELETE_USER_TEMPLATE_UUID: process.env.MAILTRAP_DELETE_USER_TEMPLATE_UUID || '', // OneSignal Push Notifications ONESIGNAL_APP_ID: process.env.ONESIGNAL_APP_ID || '', ONESIGNAL_API_KEY: process.env.ONESIGNAL_API_KEY || '', // Admin Configuration SUPER_ADMIN_EMAIL: process.env.SUPER_ADMIN_EMAIL || 'admin@yourdomain.com', SUPER_ADMIN_PASSWORD: process.env.SUPER_ADMIN_PASSWORD || 'YourSecureAdminPassword123!', // Google OAuth GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID || '', GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET || '', // Other Configuration OTP_LIMITER: process.env.OTP_LIMITER || '5', SECRET_KEY: process.env.SECRET_KEY || 'another-random-secret-key-for-general-encryption', ACCESS_KEY: process.env.ACCESS_KEY || '', DB_SYNC: process.env.DB_SYNC || 'txsrue', // Redis (used?) AZURE_REDIS_CONNECTIONSTRING: process.env.AZURE_REDIS_CONNECTIONSTRING || '', // Docker Registry (for deployment) DOCKER_REGISTRY_SERVER_URL: process.env.DOCKER_REGISTRY_SERVER_URL || '', DOCKER_REGISTRY_SERVER_USERNAME: process.env.DOCKER_REGISTRY_SERVER_USERNAME || '', DOCKER_REGISTRY_SERVER_PASSWORD: process.env.DOCKER_REGISTRY_SERVER_PASSWORD || '', // Doppler (if used for secrets management) DOPPLER_PROJECT: process.env.DOPPLER_PROJECT || '', DOPPLER_CONFIG: process.env.DOPPLER_CONFIG || '', DOPPLER_ENVIRONMENT: process.env.DOPPLER_ENVIRONMENT || '', // Azure specific WEBSITES_ENABLE_APP_SERVICE_STORAGE: process.env.WEBSITES_ENABLE_APP_SERVICE_STORAGE || 'false', }, secrets: { AZURE_POSTGRESQL_PASSWORD: ecs.Secret.fromSecretsManager( dbSecret, 'password' ), }, logDriver: ecs.LogDrivers.awsLogs({ streamPrefix: 'syncrow-backend', logGroup, }), }, publicLoadBalancer: true, securityGroups: [ecsSecurityGroup], }); // Add security group to load balancer after creation fargateService.loadBalancer.addSecurityGroup(albSecurityGroup); // Configure health check fargateService.targetGroup.configureHealthCheck({ path: '/health', healthyHttpCodes: '200', interval: cdk.Duration.seconds(30), timeout: cdk.Duration.seconds(5), healthyThresholdCount: 2, unhealthyThresholdCount: 3, }); // Auto Scaling const scalableTarget = fargateService.service.autoScaleTaskCount({ minCapacity: 1, maxCapacity: 10, }); scalableTarget.scaleOnCpuUtilization('CpuScaling', { targetUtilizationPercent: 70, scaleInCooldown: cdk.Duration.minutes(5), scaleOutCooldown: cdk.Duration.minutes(2), }); scalableTarget.scaleOnMemoryUtilization('MemoryScaling', { targetUtilizationPercent: 80, scaleInCooldown: cdk.Duration.minutes(5), scaleOutCooldown: cdk.Duration.minutes(2), }); // Grant ECS task access to RDS credentials dbSecret.grantRead(fargateService.taskDefinition.taskRole); this.apiUrl = 'https://api.syncrow.me'; this.databaseEndpoint = dbCluster.clusterEndpoint.hostname; // Outputs new cdk.CfnOutput(this, 'ApiUrl', { value: this.apiUrl, description: 'Application Load Balancer URL', exportName: `${this.stackName}-ApiUrl`, }); new cdk.CfnOutput(this, 'DatabaseEndpoint', { value: this.databaseEndpoint, description: 'RDS Cluster Endpoint', exportName: `${this.stackName}-DatabaseEndpoint`, }); new cdk.CfnOutput(this, 'EcrRepositoryUri', { value: ecrRepository.repositoryUri, description: 'ECR Repository URI', exportName: `${this.stackName}-EcrRepositoryUri`, }); new cdk.CfnOutput(this, 'ClusterName', { value: cluster.clusterName, description: 'ECS Cluster Name', exportName: `${this.stackName}-ClusterName`, }); new cdk.CfnOutput(this, 'ServiceName', { value: fargateService.service.serviceName, description: 'ECS Service Name', exportName: `${this.stackName}-ServiceName`, }); } }