mirror of
https://github.com/SyncrowIOT/backend.git
synced 2025-07-10 07:07:21 +00:00
331 lines
12 KiB
TypeScript
331 lines
12 KiB
TypeScript
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'
|
|
);
|
|
|
|
// 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'
|
|
);
|
|
|
|
// RDS Aurora Serverless v2 PostgreSQL
|
|
const dbCluster = new rds.DatabaseCluster(this, 'SyncrowDatabase', {
|
|
engine: rds.DatabaseClusterEngine.auroraPostgres({
|
|
version: rds.AuroraPostgresEngineVersion.VER_15_4,
|
|
}),
|
|
vpc: this.vpc,
|
|
securityGroups: [dbSecurityGroup],
|
|
serverlessV2MinCapacity: 0.5,
|
|
serverlessV2MaxCapacity: 4,
|
|
writer: rds.ClusterInstance.serverlessV2('writer'),
|
|
defaultDatabaseName: props?.databaseName || 'syncrow',
|
|
credentials: rds.Credentials.fromGeneratedSecret('syncrowadmin', {
|
|
secretName: 'syncrow-db-credentials',
|
|
}),
|
|
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
});
|
|
|
|
// ECR Repository for Docker images
|
|
const ecrRepository = new ecr.Repository(this, 'SyncrowBackendRepo', {
|
|
repositoryName: 'syncrow-backend',
|
|
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
emptyOnDelete: true,
|
|
});
|
|
|
|
// 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,
|
|
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: 'syncrowadmin',
|
|
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 || 'false',
|
|
|
|
// Redis (if 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(
|
|
dbCluster.secret!,
|
|
'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),
|
|
});
|
|
|
|
// For now, let's update the web app to use HTTPS URL and handle the certificate warning
|
|
// In production, you'll add a proper SSL certificate for api.syncrow.ae
|
|
|
|
// Grant ECS task access to RDS credentials
|
|
if (dbCluster.secret) {
|
|
dbCluster.secret.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`,
|
|
});
|
|
}
|
|
}
|