Compare commits

...

4 Commits

4 changed files with 242 additions and 160 deletions

View File

@ -3,19 +3,44 @@ set -e
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
REGION=${AWS_DEFAULT_REGION:-me-central-1}
REPO_NAME=syncrow-backend
IMAGE_TAG=latest
CLUSTER_NAME=syncrow-backend-cluster
STACK_NAME=SyncrowBackendStack
CERTIFICATE_ARN="arn:aws:acm:$REGION:$ACCOUNT_ID:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0"
echo "🔐 Logging into ECR..."
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com"
aws ecr get-login-password --region $REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com
echo "🐳 Building Docker image..."
docker build --platform=linux/amd64 -t $REPO_NAME .
docker build --platform=linux/amd64 -t syncrow-backend .
docker tag syncrow-backend:latest $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/syncrow-backend:latest
docker push $ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/syncrow-backend:latest
echo "🏷️ Tagging image..."
docker tag $REPO_NAME:$IMAGE_TAG "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$IMAGE_TAG"
SERVICE_ARN=$(aws ecs list-services --cluster syncrow-backend-cluster --query 'serviceArns[0]' --output text --region $REGION 2>/dev/null || echo "")
echo "📤 Pushing image to ECR..."
docker push "$ACCOUNT_ID.dkr.ecr.$REGION.amazonaws.com/$REPO_NAME:$IMAGE_TAG"
if [ "$SERVICE_ARN" != "" ] && [ "$SERVICE_ARN" != "None" ]; then
SERVICE_NAME=$(echo $SERVICE_ARN | cut -d'/' -f3)
aws ecs update-service --cluster syncrow-backend-cluster --service $SERVICE_NAME --force-new-deployment --region $REGION
else
npx cdk deploy SyncrowBackendStack --context certificateArn=arn:aws:acm:me-central-1:482311766496:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0 --require-approval never
echo "🔍 Checking if ECS service exists..."
SERVICE_ARN=$(aws ecs list-services \
--cluster $CLUSTER_NAME \
--query 'serviceArns[0]' \
--output text \
--region $REGION 2>/dev/null || echo "")
echo "📦 Deploying CDK Stack..."
npx cdk deploy $STACK_NAME \
--context certificateArn=$CERTIFICATE_ARN \
--require-approval never
if [[ "$SERVICE_ARN" != "" && "$SERVICE_ARN" != "None" ]]; then
SERVICE_NAME=$(basename "$SERVICE_ARN")
echo "🚀 Redeploying ECS Service: $SERVICE_NAME"
aws ecs update-service \
--cluster $CLUSTER_NAME \
--service $SERVICE_NAME \
--force-new-deployment \
--region $REGION
fi
echo "✅ All done."

View File

@ -1,6 +1,6 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import 'source-map-support/register';
import { BackendStack } from './stack';
const app = new cdk.App();
@ -10,6 +10,7 @@ new BackendStack(app, 'SyncrowBackendStack', {
account: process.env.CDK_DEFAULT_ACCOUNT,
region: 'me-central-1',
},
databaseName: 'syncrow',
certificateArn: 'arn:aws:acm:me-central-1:482311766496:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0',
databaseName: 'postgres',
certificateArn:
'arn:aws:acm:me-central-1:482311766496:certificate/bea1e2ae-84a1-414e-8dbf-4599397e7ed0',
});

View File

@ -1,12 +1,12 @@
import * as cdk from 'aws-cdk-lib';
import * as acm from 'aws-cdk-lib/aws-certificatemanager';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecr from 'aws-cdk-lib/aws-ecr';
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 logs from 'aws-cdk-lib/aws-logs';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as route53 from 'aws-cdk-lib/aws-route53';
import { Construct } from 'constructs';
import * as dotenv from 'dotenv';
@ -49,11 +49,15 @@ export class BackendStack extends cdk.Stack {
});
// Security Groups
const dbSecurityGroup = new ec2.SecurityGroup(this, 'DatabaseSecurityGroup', {
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,
@ -71,36 +75,39 @@ export class BackendStack extends cdk.Stack {
ecsSecurityGroup.addIngressRule(
albSecurityGroup,
ec2.Port.tcp(3000),
'Allow ALB to connect to ECS service'
'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 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'
'Temporary access from admin IP',
);
// Allow HTTP/HTTPS traffic to ALB
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(80),
'Allow HTTP traffic'
'Allow HTTP traffic',
);
albSecurityGroup.addIngressRule(
ec2.Peer.anyIpv4(),
ec2.Port.tcp(443),
'Allow HTTPS traffic'
'Allow HTTPS traffic',
);
const dbCluster = rds.DatabaseCluster.fromDatabaseClusterAttributes(this, 'SyncrowDatabase', {
const dbCluster = rds.DatabaseCluster.fromDatabaseClusterAttributes(
this,
'SyncrowDatabase',
{
clusterIdentifier: 'syncrow-backend',
instanceIdentifiers: ['syncrowdatabase-instance-1'],
engine: rds.DatabaseClusterEngine.auroraPostgres({
@ -108,18 +115,30 @@ export class BackendStack extends cdk.Stack {
}),
port: 5432,
securityGroups: [
ec2.SecurityGroup.fromSecurityGroupId(this, 'ImportedDbSecurityGroup', 'sg-07e163f588b2bac25')
ec2.SecurityGroup.fromSecurityGroupId(
this,
'ImportedDbSecurityGroup',
'sg-07e163f588b2bac25',
),
],
clusterEndpointAddress: 'syncrow-backend.cluster-criskv1sdkq4.me-central-1.rds.amazonaws.com',
});
clusterEndpointAddress:
'syncrow-backend.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'
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');
const ecrRepository = ecr.Repository.fromRepositoryName(
this,
'SyncrowBackendRepo',
'syncrow-backend',
);
// Output the correct ECR URI for this region
new cdk.CfnOutput(this, 'EcrRepositoryUriRegional', {
@ -143,14 +162,22 @@ export class BackendStack extends cdk.Stack {
// Use existing wildcard certificate or create new one
const apiCertificate = props?.certificateArn
? acm.Certificate.fromCertificateArn(this, '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', {
const fargateService =
new ecsPatterns.ApplicationLoadBalancedFargateService(
this,
'SyncrowBackendService',
{
cluster,
memoryLimitMiB: 1024,
cpu: 512,
@ -163,7 +190,10 @@ export class BackendStack extends cdk.Stack {
protocol: elbv2.ApplicationProtocol.HTTPS,
redirectHTTP: true,
taskImageOptions: {
image: ecs.ContainerImage.fromEcrRepository(ecrRepository, 'latest'),
image: ecs.ContainerImage.fromEcrRepository(
ecrRepository,
'latest',
),
containerPort: 3000,
enableLogging: true,
environment: {
@ -175,29 +205,39 @@ export class BackendStack extends cdk.Stack {
// Database connection (CDK provides these automatically)
AZURE_POSTGRESQL_HOST: dbCluster.clusterEndpoint.hostname,
AZURE_POSTGRESQL_PORT: '5432',
AZURE_POSTGRESQL_DATABASE: props?.databaseName || 'syncrow',
AZURE_POSTGRESQL_DATABASE: props?.databaseName || 'postgres',
AZURE_POSTGRESQL_USER: 'postgres',
AZURE_POSTGRESQL_SSL: process.env.AZURE_POSTGRESQL_SSL || 'false',
AZURE_POSTGRESQL_SYNC: process.env.AZURE_POSTGRESQL_SYNC || '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_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',
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_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_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_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 || '',
@ -211,19 +251,27 @@ export class BackendStack extends cdk.Stack {
// 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 || '',
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!',
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 || '',
@ -231,17 +279,23 @@ export class BackendStack extends cdk.Stack {
// Other Configuration
OTP_LIMITER: process.env.OTP_LIMITER || '5',
SECRET_KEY: process.env.SECRET_KEY || 'another-random-secret-key-for-general-encryption',
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 || '',
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 || '',
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 || '',
@ -249,12 +303,13 @@ export class BackendStack extends cdk.Stack {
DOPPLER_ENVIRONMENT: process.env.DOPPLER_ENVIRONMENT || '',
// Azure specific
WEBSITES_ENABLE_APP_SERVICE_STORAGE: process.env.WEBSITES_ENABLE_APP_SERVICE_STORAGE || 'false',
WEBSITES_ENABLE_APP_SERVICE_STORAGE:
process.env.WEBSITES_ENABLE_APP_SERVICE_STORAGE || 'false',
},
secrets: {
AZURE_POSTGRESQL_PASSWORD: ecs.Secret.fromSecretsManager(
dbSecret,
'password'
'password',
),
},
logDriver: ecs.LogDrivers.awsLogs({
@ -264,7 +319,8 @@ export class BackendStack extends cdk.Stack {
},
publicLoadBalancer: true,
securityGroups: [ecsSecurityGroup],
});
},
);
// Add security group to load balancer after creation
fargateService.loadBalancer.addSecurityGroup(albSecurityGroup);

View File

@ -22,7 +22,7 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./apps/backend/test/jest-e2e.json",
"deploy": "./deploy.sh",
"infra:build": "./build.sh",
"infra:build": "bash build.sh",
"infra:deploy": "cdk deploy SyncrowBackendStack",
"infra:destroy": "cdk destroy SyncrowBackendStack"
},