infrastructure with aws cdk
This commit is contained in:
@@ -4,11 +4,20 @@ import * as cdk from "aws-cdk-lib";
|
|||||||
import { ConditionCheckLambdaStack } from "../lib/condition-check-lambda-stack";
|
import { ConditionCheckLambdaStack } from "../lib/condition-check-lambda-stack";
|
||||||
import { ImageProcessorLambdaStack } from "../lib/image-processor-lambda-stack";
|
import { ImageProcessorLambdaStack } from "../lib/image-processor-lambda-stack";
|
||||||
import { VpcStack } from "../lib/vpc-stack";
|
import { VpcStack } from "../lib/vpc-stack";
|
||||||
|
import { CertificateStack } from "../lib/certificate-stack";
|
||||||
|
import { SecretsStack } from "../lib/secrets-stack";
|
||||||
|
import { EcrStack } from "../lib/ecr-stack";
|
||||||
|
import { RdsStack } from "../lib/rds-stack";
|
||||||
|
import { EcsServiceStack } from "../lib/ecs-service-stack";
|
||||||
|
|
||||||
const app = new cdk.App();
|
const app = new cdk.App();
|
||||||
|
|
||||||
// Get environment from context or default to staging
|
// Get environment from context or default to dev
|
||||||
const environment = app.node.tryGetContext("env") || "staging";
|
const environment = app.node.tryGetContext("env") || "dev";
|
||||||
|
|
||||||
|
// Get context variables for deployment configuration
|
||||||
|
const allowedIp = app.node.tryGetContext("allowedIp"); // e.g., "1.2.3.4/32"
|
||||||
|
const domainName = app.node.tryGetContext("domainName") || "village-share.com";
|
||||||
|
|
||||||
// Environment-specific configurations
|
// Environment-specific configurations
|
||||||
const envConfig: Record<
|
const envConfig: Record<
|
||||||
@@ -19,26 +28,46 @@ const envConfig: Record<
|
|||||||
sesFromEmail: string;
|
sesFromEmail: string;
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
natGateways: number;
|
natGateways: number;
|
||||||
|
subdomain: string;
|
||||||
|
dbInstanceType: "micro" | "small" | "medium";
|
||||||
|
multiAz: boolean;
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
|
dev: {
|
||||||
|
databaseUrl:
|
||||||
|
process.env.DATABASE_URL ||
|
||||||
|
"postgresql://user:password@localhost:5432/rentall_dev",
|
||||||
|
frontendUrl: `https://dev.${domainName}`,
|
||||||
|
sesFromEmail: `noreply@${domainName}`,
|
||||||
|
emailEnabled: false, // Disable emails in dev
|
||||||
|
natGateways: 1,
|
||||||
|
subdomain: "dev",
|
||||||
|
dbInstanceType: "micro",
|
||||||
|
multiAz: false,
|
||||||
|
},
|
||||||
staging: {
|
staging: {
|
||||||
// These should be passed via CDK context or SSM parameters in production
|
|
||||||
databaseUrl:
|
databaseUrl:
|
||||||
process.env.DATABASE_URL ||
|
process.env.DATABASE_URL ||
|
||||||
"postgresql://user:password@localhost:5432/rentall_staging",
|
"postgresql://user:password@localhost:5432/rentall_staging",
|
||||||
frontendUrl: "https://staging.villageshare.app",
|
frontendUrl: `https://staging.${domainName}`,
|
||||||
sesFromEmail: "noreply@villageshare.app",
|
sesFromEmail: `noreply@${domainName}`,
|
||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
natGateways: 1, // Single NAT gateway for cost optimization in staging
|
natGateways: 1,
|
||||||
|
subdomain: "staging",
|
||||||
|
dbInstanceType: "micro",
|
||||||
|
multiAz: false,
|
||||||
},
|
},
|
||||||
prod: {
|
prod: {
|
||||||
databaseUrl:
|
databaseUrl:
|
||||||
process.env.DATABASE_URL ||
|
process.env.DATABASE_URL ||
|
||||||
"postgresql://user:password@localhost:5432/rentall_prod",
|
"postgresql://user:password@localhost:5432/rentall_prod",
|
||||||
frontendUrl: "https://villageshare.app",
|
frontendUrl: `https://${domainName}`,
|
||||||
sesFromEmail: "noreply@villageshare.app",
|
sesFromEmail: `noreply@${domainName}`,
|
||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
natGateways: 2, // Multi-AZ NAT gateways for high availability in production
|
natGateways: 2, // Multi-AZ NAT gateways for high availability
|
||||||
|
subdomain: "", // No subdomain for prod
|
||||||
|
dbInstanceType: "small",
|
||||||
|
multiAz: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,7 +84,30 @@ const envProps = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create the VPC stack first (other stacks depend on it)
|
// Common tags for all stacks
|
||||||
|
const commonTags = {
|
||||||
|
Environment: environment,
|
||||||
|
Project: "village-share",
|
||||||
|
ManagedBy: "cdk",
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Certificate Stack (Shared across environments)
|
||||||
|
// Deploy this once and validate DNS before deploying other stacks
|
||||||
|
// ============================================================================
|
||||||
|
const certificateStack = new CertificateStack(app, "CertificateStack", {
|
||||||
|
domainName,
|
||||||
|
...envProps,
|
||||||
|
description: `ACM wildcard certificate for ${domainName}`,
|
||||||
|
tags: {
|
||||||
|
...commonTags,
|
||||||
|
Service: "certificate",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// VPC Stack
|
||||||
|
// ============================================================================
|
||||||
const vpcStack = new VpcStack(app, `VpcStack-${environment}`, {
|
const vpcStack = new VpcStack(app, `VpcStack-${environment}`, {
|
||||||
environment,
|
environment,
|
||||||
natGateways: config.natGateways,
|
natGateways: config.natGateways,
|
||||||
@@ -63,13 +115,102 @@ const vpcStack = new VpcStack(app, `VpcStack-${environment}`, {
|
|||||||
...envProps,
|
...envProps,
|
||||||
description: `VPC infrastructure with private subnets and VPC endpoints (${environment})`,
|
description: `VPC infrastructure with private subnets and VPC endpoints (${environment})`,
|
||||||
tags: {
|
tags: {
|
||||||
Environment: environment,
|
...commonTags,
|
||||||
Project: "village-share",
|
|
||||||
Service: "networking",
|
Service: "networking",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create the Condition Check Lambda stack
|
// ============================================================================
|
||||||
|
// Secrets Stack
|
||||||
|
// ============================================================================
|
||||||
|
const secretsStack = new SecretsStack(app, `SecretsStack-${environment}`, {
|
||||||
|
environment,
|
||||||
|
...envProps,
|
||||||
|
description: `Secrets Manager secrets for database and application (${environment})`,
|
||||||
|
tags: {
|
||||||
|
...commonTags,
|
||||||
|
Service: "secrets",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ECR Stack
|
||||||
|
// ============================================================================
|
||||||
|
const ecrStack = new EcrStack(app, `EcrStack-${environment}`, {
|
||||||
|
environment,
|
||||||
|
...envProps,
|
||||||
|
description: `ECR repositories for Docker images (${environment})`,
|
||||||
|
tags: {
|
||||||
|
...commonTags,
|
||||||
|
Service: "ecr",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// RDS Stack
|
||||||
|
// ============================================================================
|
||||||
|
const rdsStack = new RdsStack(app, `RdsStack-${environment}`, {
|
||||||
|
environment,
|
||||||
|
vpc: vpcStack.vpc,
|
||||||
|
databaseSecret: secretsStack.databaseSecret,
|
||||||
|
databaseName: "rentall",
|
||||||
|
multiAz: config.multiAz,
|
||||||
|
...envProps,
|
||||||
|
description: `RDS PostgreSQL database (${environment})`,
|
||||||
|
tags: {
|
||||||
|
...commonTags,
|
||||||
|
Service: "database",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// RDS depends on VPC and Secrets
|
||||||
|
rdsStack.addDependency(vpcStack);
|
||||||
|
rdsStack.addDependency(secretsStack);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// ECS Service Stack
|
||||||
|
// ============================================================================
|
||||||
|
const fullDomainName = config.subdomain
|
||||||
|
? `${config.subdomain}.${domainName}`
|
||||||
|
: domainName;
|
||||||
|
|
||||||
|
const ecsServiceStack = new EcsServiceStack(
|
||||||
|
app,
|
||||||
|
`EcsServiceStack-${environment}`,
|
||||||
|
{
|
||||||
|
environment,
|
||||||
|
vpc: vpcStack.vpc,
|
||||||
|
certificate: certificateStack.certificate,
|
||||||
|
backendRepository: ecrStack.backendRepository,
|
||||||
|
frontendRepository: ecrStack.frontendRepository,
|
||||||
|
databaseSecret: secretsStack.databaseSecret,
|
||||||
|
appSecret: secretsStack.appSecret,
|
||||||
|
databaseSecurityGroup: rdsStack.databaseSecurityGroup,
|
||||||
|
dbEndpoint: rdsStack.dbEndpoint,
|
||||||
|
dbPort: rdsStack.dbPort,
|
||||||
|
dbName: "rentall",
|
||||||
|
domainName: fullDomainName,
|
||||||
|
allowedIp: environment === "dev" ? allowedIp : undefined, // Only restrict in dev
|
||||||
|
frontendUrl: config.frontendUrl,
|
||||||
|
...envProps,
|
||||||
|
description: `ECS Fargate services with ALB (${environment})`,
|
||||||
|
tags: {
|
||||||
|
...commonTags,
|
||||||
|
Service: "ecs",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// ECS depends on VPC, Certificate, ECR, Secrets, and RDS
|
||||||
|
ecsServiceStack.addDependency(vpcStack);
|
||||||
|
ecsServiceStack.addDependency(certificateStack);
|
||||||
|
ecsServiceStack.addDependency(ecrStack);
|
||||||
|
ecsServiceStack.addDependency(secretsStack);
|
||||||
|
ecsServiceStack.addDependency(rdsStack);
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// Lambda Stacks (existing)
|
||||||
|
// ============================================================================
|
||||||
const conditionCheckStack = new ConditionCheckLambdaStack(
|
const conditionCheckStack = new ConditionCheckLambdaStack(
|
||||||
app,
|
app,
|
||||||
`ConditionCheckLambdaStack-${environment}`,
|
`ConditionCheckLambdaStack-${environment}`,
|
||||||
@@ -84,17 +225,14 @@ const conditionCheckStack = new ConditionCheckLambdaStack(
|
|||||||
...envProps,
|
...envProps,
|
||||||
description: `Condition Check Reminder Lambda infrastructure (${environment})`,
|
description: `Condition Check Reminder Lambda infrastructure (${environment})`,
|
||||||
tags: {
|
tags: {
|
||||||
Environment: environment,
|
...commonTags,
|
||||||
Project: "village-share",
|
|
||||||
Service: "condition-check-reminder",
|
Service: "condition-check-reminder",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add dependency on VPC stack
|
|
||||||
conditionCheckStack.addDependency(vpcStack);
|
conditionCheckStack.addDependency(vpcStack);
|
||||||
|
|
||||||
// Create the Image Processor Lambda stack
|
|
||||||
const imageProcessorStack = new ImageProcessorLambdaStack(
|
const imageProcessorStack = new ImageProcessorLambdaStack(
|
||||||
app,
|
app,
|
||||||
`ImageProcessorLambdaStack-${environment}`,
|
`ImageProcessorLambdaStack-${environment}`,
|
||||||
@@ -107,12 +245,10 @@ const imageProcessorStack = new ImageProcessorLambdaStack(
|
|||||||
...envProps,
|
...envProps,
|
||||||
description: `Image Processor Lambda infrastructure (${environment})`,
|
description: `Image Processor Lambda infrastructure (${environment})`,
|
||||||
tags: {
|
tags: {
|
||||||
Environment: environment,
|
...commonTags,
|
||||||
Project: "village-share",
|
|
||||||
Service: "image-processor",
|
Service: "image-processor",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add dependency on VPC stack
|
|
||||||
imageProcessorStack.addDependency(vpcStack);
|
imageProcessorStack.addDependency(vpcStack);
|
||||||
|
|||||||
59
infrastructure/cdk/lib/certificate-stack.ts
Normal file
59
infrastructure/cdk/lib/certificate-stack.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as acm from "aws-cdk-lib/aws-certificatemanager";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
interface CertificateStackProps extends cdk.StackProps {
|
||||||
|
/**
|
||||||
|
* The domain name for the certificate (e.g., village-share.com)
|
||||||
|
*/
|
||||||
|
domainName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CertificateStack extends cdk.Stack {
|
||||||
|
/**
|
||||||
|
* The ACM certificate for the domain
|
||||||
|
*/
|
||||||
|
public readonly certificate: acm.Certificate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The certificate ARN for cross-stack references
|
||||||
|
*/
|
||||||
|
public readonly certificateArn: string;
|
||||||
|
|
||||||
|
constructor(scope: Construct, id: string, props: CertificateStackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const { domainName } = props;
|
||||||
|
|
||||||
|
// Create wildcard certificate for the domain
|
||||||
|
// This covers both the apex domain and all subdomains
|
||||||
|
this.certificate = new acm.Certificate(this, "WildcardCertificate", {
|
||||||
|
domainName: domainName,
|
||||||
|
subjectAlternativeNames: [`*.${domainName}`],
|
||||||
|
validation: acm.CertificateValidation.fromDns(),
|
||||||
|
certificateName: `${domainName}-wildcard`,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.certificateArn = this.certificate.certificateArn;
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
new cdk.CfnOutput(this, "CertificateArn", {
|
||||||
|
value: this.certificate.certificateArn,
|
||||||
|
description: "ACM Certificate ARN",
|
||||||
|
exportName: `CertificateArn-${domainName.replace(/\./g, "-")}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "DomainName", {
|
||||||
|
value: domainName,
|
||||||
|
description: "Domain name for the certificate",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Important: After deployment, you need to add CNAME records to your DNS provider
|
||||||
|
// Run: aws acm describe-certificate --certificate-arn <ARN> --query 'Certificate.DomainValidationOptions'
|
||||||
|
// to get the CNAME records needed for DNS validation
|
||||||
|
new cdk.CfnOutput(this, "ValidationInstructions", {
|
||||||
|
value: `Run 'aws acm describe-certificate --certificate-arn ${this.certificate.certificateArn} --query Certificate.DomainValidationOptions' to get DNS validation records`,
|
||||||
|
description: "Instructions for DNS validation",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
90
infrastructure/cdk/lib/ecr-stack.ts
Normal file
90
infrastructure/cdk/lib/ecr-stack.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as ecr from "aws-cdk-lib/aws-ecr";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
interface EcrStackProps extends cdk.StackProps {
|
||||||
|
/**
|
||||||
|
* Environment name (dev, staging, prod)
|
||||||
|
*/
|
||||||
|
environment: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of images to retain (default: 10)
|
||||||
|
*/
|
||||||
|
maxImageCount?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EcrStack extends cdk.Stack {
|
||||||
|
/**
|
||||||
|
* Backend Docker image repository
|
||||||
|
*/
|
||||||
|
public readonly backendRepository: ecr.Repository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frontend Docker image repository
|
||||||
|
*/
|
||||||
|
public readonly frontendRepository: ecr.Repository;
|
||||||
|
|
||||||
|
constructor(scope: Construct, id: string, props: EcrStackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const { environment, maxImageCount = 10 } = props;
|
||||||
|
|
||||||
|
// Backend repository
|
||||||
|
this.backendRepository = new ecr.Repository(this, "BackendRepository", {
|
||||||
|
repositoryName: `rentall-backend-${environment}`,
|
||||||
|
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||||
|
imageScanOnPush: true,
|
||||||
|
imageTagMutability: ecr.TagMutability.MUTABLE,
|
||||||
|
lifecycleRules: [
|
||||||
|
{
|
||||||
|
rulePriority: 1,
|
||||||
|
description: `Keep only the last ${maxImageCount} images`,
|
||||||
|
maxImageCount: maxImageCount,
|
||||||
|
tagStatus: ecr.TagStatus.ANY,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Frontend repository
|
||||||
|
this.frontendRepository = new ecr.Repository(this, "FrontendRepository", {
|
||||||
|
repositoryName: `rentall-frontend-${environment}`,
|
||||||
|
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
||||||
|
imageScanOnPush: true,
|
||||||
|
imageTagMutability: ecr.TagMutability.MUTABLE,
|
||||||
|
lifecycleRules: [
|
||||||
|
{
|
||||||
|
rulePriority: 1,
|
||||||
|
description: `Keep only the last ${maxImageCount} images`,
|
||||||
|
maxImageCount: maxImageCount,
|
||||||
|
tagStatus: ecr.TagStatus.ANY,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
new cdk.CfnOutput(this, "BackendRepositoryUri", {
|
||||||
|
value: this.backendRepository.repositoryUri,
|
||||||
|
description: "Backend ECR repository URI",
|
||||||
|
exportName: `BackendRepositoryUri-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "BackendRepositoryName", {
|
||||||
|
value: this.backendRepository.repositoryName,
|
||||||
|
description: "Backend ECR repository name",
|
||||||
|
exportName: `BackendRepositoryName-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "FrontendRepositoryUri", {
|
||||||
|
value: this.frontendRepository.repositoryUri,
|
||||||
|
description: "Frontend ECR repository URI",
|
||||||
|
exportName: `FrontendRepositoryUri-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "FrontendRepositoryName", {
|
||||||
|
value: this.frontendRepository.repositoryName,
|
||||||
|
description: "Frontend ECR repository name",
|
||||||
|
exportName: `FrontendRepositoryName-${environment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
483
infrastructure/cdk/lib/ecs-service-stack.ts
Normal file
483
infrastructure/cdk/lib/ecs-service-stack.ts
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
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 ecr from "aws-cdk-lib/aws-ecr";
|
||||||
|
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
|
||||||
|
import * as logs from "aws-cdk-lib/aws-logs";
|
||||||
|
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
||||||
|
import * as acm from "aws-cdk-lib/aws-certificatemanager";
|
||||||
|
import * as iam from "aws-cdk-lib/aws-iam";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
interface EcsServiceStackProps extends cdk.StackProps {
|
||||||
|
/**
|
||||||
|
* Environment name (dev, staging, prod)
|
||||||
|
*/
|
||||||
|
environment: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VPC to deploy services in
|
||||||
|
*/
|
||||||
|
vpc: ec2.IVpc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ACM certificate for HTTPS
|
||||||
|
*/
|
||||||
|
certificate: acm.ICertificate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend ECR repository
|
||||||
|
*/
|
||||||
|
backendRepository: ecr.IRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frontend ECR repository
|
||||||
|
*/
|
||||||
|
frontendRepository: ecr.IRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database credentials secret
|
||||||
|
*/
|
||||||
|
databaseSecret: secretsmanager.ISecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application secrets (JWT, etc.)
|
||||||
|
*/
|
||||||
|
appSecret: secretsmanager.ISecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database security group (to allow ECS -> RDS access)
|
||||||
|
*/
|
||||||
|
databaseSecurityGroup: ec2.ISecurityGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database endpoint
|
||||||
|
*/
|
||||||
|
dbEndpoint: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database port
|
||||||
|
*/
|
||||||
|
dbPort: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database name
|
||||||
|
*/
|
||||||
|
dbName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Domain name for the environment (e.g., dev.village-share.com)
|
||||||
|
*/
|
||||||
|
domainName: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IP address to restrict ALB access to (CIDR format, e.g., "1.2.3.4/32")
|
||||||
|
* If not provided, ALB is open to the internet
|
||||||
|
*/
|
||||||
|
allowedIp?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frontend URL for CORS configuration
|
||||||
|
*/
|
||||||
|
frontendUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class EcsServiceStack extends cdk.Stack {
|
||||||
|
/**
|
||||||
|
* The ECS cluster
|
||||||
|
*/
|
||||||
|
public readonly cluster: ecs.Cluster;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Application Load Balancer
|
||||||
|
*/
|
||||||
|
public readonly alb: elbv2.ApplicationLoadBalancer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend ECS service
|
||||||
|
*/
|
||||||
|
public readonly backendService: ecs.FargateService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frontend ECS service
|
||||||
|
*/
|
||||||
|
public readonly frontendService: ecs.FargateService;
|
||||||
|
|
||||||
|
constructor(scope: Construct, id: string, props: EcsServiceStackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const {
|
||||||
|
environment,
|
||||||
|
vpc,
|
||||||
|
certificate,
|
||||||
|
backendRepository,
|
||||||
|
frontendRepository,
|
||||||
|
databaseSecret,
|
||||||
|
appSecret,
|
||||||
|
databaseSecurityGroup,
|
||||||
|
dbEndpoint,
|
||||||
|
dbPort,
|
||||||
|
dbName,
|
||||||
|
domainName,
|
||||||
|
allowedIp,
|
||||||
|
frontendUrl,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// ECS Cluster with Container Insights
|
||||||
|
this.cluster = new ecs.Cluster(this, "Cluster", {
|
||||||
|
clusterName: `rentall-cluster-${environment}`,
|
||||||
|
vpc,
|
||||||
|
containerInsights: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// ALB Security Group
|
||||||
|
const albSecurityGroup = new ec2.SecurityGroup(this, "AlbSecurityGroup", {
|
||||||
|
vpc,
|
||||||
|
securityGroupName: `rentall-alb-sg-${environment}`,
|
||||||
|
description: `ALB security group for rentall ${environment}`,
|
||||||
|
allowAllOutbound: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure ALB access based on allowedIp
|
||||||
|
if (allowedIp) {
|
||||||
|
// Restrict to specific IP (dev environment)
|
||||||
|
albSecurityGroup.addIngressRule(
|
||||||
|
ec2.Peer.ipv4(allowedIp),
|
||||||
|
ec2.Port.tcp(443),
|
||||||
|
`Allow HTTPS from ${allowedIp}`
|
||||||
|
);
|
||||||
|
albSecurityGroup.addIngressRule(
|
||||||
|
ec2.Peer.ipv4(allowedIp),
|
||||||
|
ec2.Port.tcp(80),
|
||||||
|
`Allow HTTP from ${allowedIp} (for redirect)`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Open to the internet (staging/prod)
|
||||||
|
albSecurityGroup.addIngressRule(
|
||||||
|
ec2.Peer.anyIpv4(),
|
||||||
|
ec2.Port.tcp(443),
|
||||||
|
"Allow HTTPS from anywhere"
|
||||||
|
);
|
||||||
|
albSecurityGroup.addIngressRule(
|
||||||
|
ec2.Peer.anyIpv4(),
|
||||||
|
ec2.Port.tcp(80),
|
||||||
|
"Allow HTTP from anywhere (for redirect)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application Load Balancer
|
||||||
|
this.alb = new elbv2.ApplicationLoadBalancer(this, "Alb", {
|
||||||
|
loadBalancerName: `rentall-alb-${environment}`,
|
||||||
|
vpc,
|
||||||
|
internetFacing: true,
|
||||||
|
securityGroup: albSecurityGroup,
|
||||||
|
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTTPS Listener (port 443)
|
||||||
|
const httpsListener = this.alb.addListener("HttpsListener", {
|
||||||
|
port: 443,
|
||||||
|
protocol: elbv2.ApplicationProtocol.HTTPS,
|
||||||
|
certificates: [certificate],
|
||||||
|
sslPolicy: elbv2.SslPolicy.TLS12,
|
||||||
|
});
|
||||||
|
|
||||||
|
// HTTP Listener (port 80) - Redirect to HTTPS
|
||||||
|
this.alb.addListener("HttpListener", {
|
||||||
|
port: 80,
|
||||||
|
protocol: elbv2.ApplicationProtocol.HTTP,
|
||||||
|
defaultAction: elbv2.ListenerAction.redirect({
|
||||||
|
protocol: "HTTPS",
|
||||||
|
port: "443",
|
||||||
|
permanent: true,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backend Security Group
|
||||||
|
const backendSecurityGroup = new ec2.SecurityGroup(
|
||||||
|
this,
|
||||||
|
"BackendSecurityGroup",
|
||||||
|
{
|
||||||
|
vpc,
|
||||||
|
securityGroupName: `rentall-backend-sg-${environment}`,
|
||||||
|
description: `Backend service security group (${environment})`,
|
||||||
|
allowAllOutbound: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow ALB to reach backend
|
||||||
|
backendSecurityGroup.addIngressRule(
|
||||||
|
albSecurityGroup,
|
||||||
|
ec2.Port.tcp(5000),
|
||||||
|
"Allow traffic from ALB"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow backend to reach database
|
||||||
|
databaseSecurityGroup.addIngressRule(
|
||||||
|
backendSecurityGroup,
|
||||||
|
ec2.Port.tcp(dbPort),
|
||||||
|
"Allow traffic from backend ECS"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Frontend Security Group
|
||||||
|
const frontendSecurityGroup = new ec2.SecurityGroup(
|
||||||
|
this,
|
||||||
|
"FrontendSecurityGroup",
|
||||||
|
{
|
||||||
|
vpc,
|
||||||
|
securityGroupName: `rentall-frontend-sg-${environment}`,
|
||||||
|
description: `Frontend service security group (${environment})`,
|
||||||
|
allowAllOutbound: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow ALB to reach frontend
|
||||||
|
frontendSecurityGroup.addIngressRule(
|
||||||
|
albSecurityGroup,
|
||||||
|
ec2.Port.tcp(80),
|
||||||
|
"Allow traffic from ALB"
|
||||||
|
);
|
||||||
|
|
||||||
|
// CloudWatch Log Groups
|
||||||
|
const backendLogGroup = new logs.LogGroup(this, "BackendLogGroup", {
|
||||||
|
logGroupName: `/ecs/rentall-backend-${environment}`,
|
||||||
|
retention: logs.RetentionDays.ONE_MONTH,
|
||||||
|
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||||
|
});
|
||||||
|
|
||||||
|
const frontendLogGroup = new logs.LogGroup(this, "FrontendLogGroup", {
|
||||||
|
logGroupName: `/ecs/rentall-frontend-${environment}`,
|
||||||
|
retention: logs.RetentionDays.ONE_MONTH,
|
||||||
|
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backend Task Definition
|
||||||
|
const backendTaskDef = new ecs.FargateTaskDefinition(
|
||||||
|
this,
|
||||||
|
"BackendTaskDef",
|
||||||
|
{
|
||||||
|
family: `rentall-backend-${environment}`,
|
||||||
|
cpu: 512, // 0.5 vCPU
|
||||||
|
memoryLimitMiB: 1024, // 1 GB
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Grant secrets access to backend task
|
||||||
|
databaseSecret.grantRead(backendTaskDef.taskRole);
|
||||||
|
appSecret.grantRead(backendTaskDef.taskRole);
|
||||||
|
|
||||||
|
// Backend container
|
||||||
|
const backendContainer = backendTaskDef.addContainer("backend", {
|
||||||
|
containerName: "backend",
|
||||||
|
image: ecs.ContainerImage.fromEcrRepository(backendRepository, "latest"),
|
||||||
|
logging: ecs.LogDriver.awsLogs({
|
||||||
|
logGroup: backendLogGroup,
|
||||||
|
streamPrefix: "backend",
|
||||||
|
}),
|
||||||
|
environment: {
|
||||||
|
NODE_ENV: environment === "prod" ? "production" : "development",
|
||||||
|
PORT: "5000",
|
||||||
|
DB_HOST: dbEndpoint,
|
||||||
|
DB_PORT: dbPort.toString(),
|
||||||
|
DB_NAME: dbName,
|
||||||
|
FRONTEND_URL: frontendUrl,
|
||||||
|
CORS_ORIGIN: frontendUrl,
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
DB_USER: ecs.Secret.fromSecretsManager(databaseSecret, "username"),
|
||||||
|
DB_PASSWORD: ecs.Secret.fromSecretsManager(databaseSecret, "password"),
|
||||||
|
JWT_SECRET: ecs.Secret.fromSecretsManager(appSecret, "jwtSecret"),
|
||||||
|
},
|
||||||
|
portMappings: [
|
||||||
|
{
|
||||||
|
containerPort: 5000,
|
||||||
|
protocol: ecs.Protocol.TCP,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
healthCheck: {
|
||||||
|
command: [
|
||||||
|
"CMD-SHELL",
|
||||||
|
"curl -f http://localhost:5000/api/health || exit 1",
|
||||||
|
],
|
||||||
|
interval: cdk.Duration.seconds(30),
|
||||||
|
timeout: cdk.Duration.seconds(5),
|
||||||
|
retries: 3,
|
||||||
|
startPeriod: cdk.Duration.seconds(60),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backend Service
|
||||||
|
this.backendService = new ecs.FargateService(this, "BackendService", {
|
||||||
|
serviceName: `backend-${environment}`,
|
||||||
|
cluster: this.cluster,
|
||||||
|
taskDefinition: backendTaskDef,
|
||||||
|
desiredCount: 1,
|
||||||
|
securityGroups: [backendSecurityGroup],
|
||||||
|
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
|
||||||
|
enableExecuteCommand: true, // Enable ECS Exec for debugging/migrations
|
||||||
|
circuitBreaker: { rollback: true },
|
||||||
|
minHealthyPercent: 100,
|
||||||
|
maxHealthyPercent: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Frontend Task Definition (Fargate Spot for cost savings)
|
||||||
|
const frontendTaskDef = new ecs.FargateTaskDefinition(
|
||||||
|
this,
|
||||||
|
"FrontendTaskDef",
|
||||||
|
{
|
||||||
|
family: `rentall-frontend-${environment}`,
|
||||||
|
cpu: 256, // 0.25 vCPU
|
||||||
|
memoryLimitMiB: 512, // 512 MB
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Frontend container
|
||||||
|
const frontendContainer = frontendTaskDef.addContainer("frontend", {
|
||||||
|
containerName: "frontend",
|
||||||
|
image: ecs.ContainerImage.fromEcrRepository(frontendRepository, "latest"),
|
||||||
|
logging: ecs.LogDriver.awsLogs({
|
||||||
|
logGroup: frontendLogGroup,
|
||||||
|
streamPrefix: "frontend",
|
||||||
|
}),
|
||||||
|
portMappings: [
|
||||||
|
{
|
||||||
|
containerPort: 80,
|
||||||
|
protocol: ecs.Protocol.TCP,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
healthCheck: {
|
||||||
|
command: ["CMD-SHELL", "curl -f http://localhost:80/ || exit 1"],
|
||||||
|
interval: cdk.Duration.seconds(30),
|
||||||
|
timeout: cdk.Duration.seconds(5),
|
||||||
|
retries: 3,
|
||||||
|
startPeriod: cdk.Duration.seconds(30),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Frontend Service (using Fargate Spot for 70% cost savings)
|
||||||
|
this.frontendService = new ecs.FargateService(this, "FrontendService", {
|
||||||
|
serviceName: `frontend-${environment}`,
|
||||||
|
cluster: this.cluster,
|
||||||
|
taskDefinition: frontendTaskDef,
|
||||||
|
desiredCount: 1,
|
||||||
|
securityGroups: [frontendSecurityGroup],
|
||||||
|
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
|
||||||
|
capacityProviderStrategies: [
|
||||||
|
{
|
||||||
|
capacityProvider: "FARGATE_SPOT",
|
||||||
|
weight: 1,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
circuitBreaker: { rollback: true },
|
||||||
|
minHealthyPercent: 100,
|
||||||
|
maxHealthyPercent: 200,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Backend Target Group
|
||||||
|
const backendTargetGroup = new elbv2.ApplicationTargetGroup(
|
||||||
|
this,
|
||||||
|
"BackendTargetGroup",
|
||||||
|
{
|
||||||
|
targetGroupName: `backend-tg-${environment}`,
|
||||||
|
vpc,
|
||||||
|
port: 5000,
|
||||||
|
protocol: elbv2.ApplicationProtocol.HTTP,
|
||||||
|
targetType: elbv2.TargetType.IP,
|
||||||
|
healthCheck: {
|
||||||
|
path: "/api/health",
|
||||||
|
healthyHttpCodes: "200",
|
||||||
|
interval: cdk.Duration.seconds(30),
|
||||||
|
timeout: cdk.Duration.seconds(5),
|
||||||
|
healthyThresholdCount: 2,
|
||||||
|
unhealthyThresholdCount: 3,
|
||||||
|
},
|
||||||
|
deregistrationDelay: cdk.Duration.seconds(30),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register backend service with target group
|
||||||
|
this.backendService.attachToApplicationTargetGroup(backendTargetGroup);
|
||||||
|
|
||||||
|
// Frontend Target Group
|
||||||
|
const frontendTargetGroup = new elbv2.ApplicationTargetGroup(
|
||||||
|
this,
|
||||||
|
"FrontendTargetGroup",
|
||||||
|
{
|
||||||
|
targetGroupName: `frontend-tg-${environment}`,
|
||||||
|
vpc,
|
||||||
|
port: 80,
|
||||||
|
protocol: elbv2.ApplicationProtocol.HTTP,
|
||||||
|
targetType: elbv2.TargetType.IP,
|
||||||
|
healthCheck: {
|
||||||
|
path: "/",
|
||||||
|
healthyHttpCodes: "200",
|
||||||
|
interval: cdk.Duration.seconds(30),
|
||||||
|
timeout: cdk.Duration.seconds(5),
|
||||||
|
healthyThresholdCount: 2,
|
||||||
|
unhealthyThresholdCount: 3,
|
||||||
|
},
|
||||||
|
deregistrationDelay: cdk.Duration.seconds(30),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Register frontend service with target group
|
||||||
|
this.frontendService.attachToApplicationTargetGroup(frontendTargetGroup);
|
||||||
|
|
||||||
|
// Configure listener rules for path-based routing
|
||||||
|
// /api/* -> backend
|
||||||
|
httpsListener.addTargetGroups("BackendRule", {
|
||||||
|
targetGroups: [backendTargetGroup],
|
||||||
|
priority: 10,
|
||||||
|
conditions: [elbv2.ListenerCondition.pathPatterns(["/api/*"])],
|
||||||
|
});
|
||||||
|
|
||||||
|
// /* -> frontend (default)
|
||||||
|
httpsListener.addTargetGroups("FrontendRule", {
|
||||||
|
targetGroups: [frontendTargetGroup],
|
||||||
|
priority: 20,
|
||||||
|
conditions: [elbv2.ListenerCondition.pathPatterns(["/*"])],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
new cdk.CfnOutput(this, "ClusterName", {
|
||||||
|
value: this.cluster.clusterName,
|
||||||
|
description: "ECS Cluster name",
|
||||||
|
exportName: `ClusterName-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "AlbDnsName", {
|
||||||
|
value: this.alb.loadBalancerDnsName,
|
||||||
|
description: "ALB DNS name - add CNAME record pointing to this",
|
||||||
|
exportName: `AlbDnsName-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "AlbArn", {
|
||||||
|
value: this.alb.loadBalancerArn,
|
||||||
|
description: "ALB ARN",
|
||||||
|
exportName: `AlbArn-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "ServiceUrl", {
|
||||||
|
value: `https://${domainName}`,
|
||||||
|
description: "Service URL",
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "BackendServiceName", {
|
||||||
|
value: this.backendService.serviceName,
|
||||||
|
description: "Backend service name",
|
||||||
|
exportName: `BackendServiceName-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "FrontendServiceName", {
|
||||||
|
value: this.frontendService.serviceName,
|
||||||
|
description: "Frontend service name",
|
||||||
|
exportName: `FrontendServiceName-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Instructions for accessing the service
|
||||||
|
new cdk.CfnOutput(this, "DnsInstructions", {
|
||||||
|
value: `Add CNAME record: ${domainName} -> ${this.alb.loadBalancerDnsName}`,
|
||||||
|
description: "DNS configuration instructions",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
174
infrastructure/cdk/lib/rds-stack.ts
Normal file
174
infrastructure/cdk/lib/rds-stack.ts
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
||||||
|
import * as rds from "aws-cdk-lib/aws-rds";
|
||||||
|
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
interface RdsStackProps extends cdk.StackProps {
|
||||||
|
/**
|
||||||
|
* Environment name (dev, staging, prod)
|
||||||
|
*/
|
||||||
|
environment: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VPC to deploy the database in
|
||||||
|
*/
|
||||||
|
vpc: ec2.IVpc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database credentials secret from SecretsStack
|
||||||
|
*/
|
||||||
|
databaseSecret: secretsmanager.ISecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database name (default: rentall)
|
||||||
|
*/
|
||||||
|
databaseName?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instance type (default: t3.micro for Free Tier)
|
||||||
|
*/
|
||||||
|
instanceType?: ec2.InstanceType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allocated storage in GB (default: 20)
|
||||||
|
*/
|
||||||
|
allocatedStorage?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable Multi-AZ deployment (default: false for dev/staging)
|
||||||
|
*/
|
||||||
|
multiAz?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup retention days (default: 7)
|
||||||
|
*/
|
||||||
|
backupRetentionDays?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RdsStack extends cdk.Stack {
|
||||||
|
/**
|
||||||
|
* The RDS database instance
|
||||||
|
*/
|
||||||
|
public readonly database: rds.DatabaseInstance;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security group for the database
|
||||||
|
*/
|
||||||
|
public readonly databaseSecurityGroup: ec2.SecurityGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database endpoint address
|
||||||
|
*/
|
||||||
|
public readonly dbEndpoint: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database port
|
||||||
|
*/
|
||||||
|
public readonly dbPort: number;
|
||||||
|
|
||||||
|
constructor(scope: Construct, id: string, props: RdsStackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const {
|
||||||
|
environment,
|
||||||
|
vpc,
|
||||||
|
databaseSecret,
|
||||||
|
databaseName = "rentall",
|
||||||
|
instanceType = ec2.InstanceType.of(
|
||||||
|
ec2.InstanceClass.T3,
|
||||||
|
ec2.InstanceSize.MICRO
|
||||||
|
),
|
||||||
|
allocatedStorage = 20,
|
||||||
|
multiAz = false,
|
||||||
|
backupRetentionDays = 7,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
// Security group for the database
|
||||||
|
this.databaseSecurityGroup = new ec2.SecurityGroup(
|
||||||
|
this,
|
||||||
|
"DatabaseSecurityGroup",
|
||||||
|
{
|
||||||
|
vpc,
|
||||||
|
securityGroupName: `rentall-db-sg-${environment}`,
|
||||||
|
description: `Security group for RDS database (${environment})`,
|
||||||
|
allowAllOutbound: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create the RDS instance
|
||||||
|
this.database = new rds.DatabaseInstance(this, "Database", {
|
||||||
|
instanceIdentifier: `rentall-db-${environment}`,
|
||||||
|
engine: rds.DatabaseInstanceEngine.postgres({
|
||||||
|
version: rds.PostgresEngineVersion.VER_15,
|
||||||
|
}),
|
||||||
|
instanceType,
|
||||||
|
vpc,
|
||||||
|
vpcSubnets: {
|
||||||
|
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
|
||||||
|
},
|
||||||
|
securityGroups: [this.databaseSecurityGroup],
|
||||||
|
credentials: rds.Credentials.fromSecret(databaseSecret),
|
||||||
|
databaseName,
|
||||||
|
allocatedStorage,
|
||||||
|
maxAllocatedStorage: allocatedStorage * 2, // Allow storage autoscaling up to 2x
|
||||||
|
storageType: rds.StorageType.GP2,
|
||||||
|
multiAz,
|
||||||
|
autoMinorVersionUpgrade: true,
|
||||||
|
deletionProtection: environment === "prod",
|
||||||
|
removalPolicy:
|
||||||
|
environment === "prod"
|
||||||
|
? cdk.RemovalPolicy.RETAIN
|
||||||
|
: cdk.RemovalPolicy.DESTROY,
|
||||||
|
backupRetention: cdk.Duration.days(backupRetentionDays),
|
||||||
|
preferredBackupWindow: "03:00-04:00", // UTC
|
||||||
|
preferredMaintenanceWindow: "Sun:04:00-Sun:05:00", // UTC
|
||||||
|
storageEncrypted: true,
|
||||||
|
monitoringInterval: cdk.Duration.seconds(60),
|
||||||
|
enablePerformanceInsights: true,
|
||||||
|
performanceInsightRetention: rds.PerformanceInsightRetention.DEFAULT, // 7 days (free)
|
||||||
|
parameterGroup: new rds.ParameterGroup(this, "ParameterGroup", {
|
||||||
|
engine: rds.DatabaseInstanceEngine.postgres({
|
||||||
|
version: rds.PostgresEngineVersion.VER_15,
|
||||||
|
}),
|
||||||
|
parameters: {
|
||||||
|
// Enforce SSL connections
|
||||||
|
"rds.force_ssl": "1",
|
||||||
|
// Log slow queries (> 1 second)
|
||||||
|
log_min_duration_statement: "1000",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
publiclyAccessible: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.dbEndpoint = this.database.dbInstanceEndpointAddress;
|
||||||
|
this.dbPort = this.database.dbInstanceEndpointPort
|
||||||
|
? parseInt(this.database.dbInstanceEndpointPort)
|
||||||
|
: 5432;
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
new cdk.CfnOutput(this, "DatabaseEndpoint", {
|
||||||
|
value: this.database.dbInstanceEndpointAddress,
|
||||||
|
description: "Database endpoint address",
|
||||||
|
exportName: `DatabaseEndpoint-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "DatabasePort", {
|
||||||
|
value: this.database.dbInstanceEndpointPort,
|
||||||
|
description: "Database port",
|
||||||
|
exportName: `DatabasePort-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "DatabaseSecurityGroupId", {
|
||||||
|
value: this.databaseSecurityGroup.securityGroupId,
|
||||||
|
description: "Database security group ID",
|
||||||
|
exportName: `DatabaseSecurityGroupId-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "DatabaseInstanceIdentifier", {
|
||||||
|
value: this.database.instanceIdentifier,
|
||||||
|
description: "Database instance identifier",
|
||||||
|
exportName: `DatabaseInstanceIdentifier-${environment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
87
infrastructure/cdk/lib/secrets-stack.ts
Normal file
87
infrastructure/cdk/lib/secrets-stack.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
interface SecretsStackProps extends cdk.StackProps {
|
||||||
|
/**
|
||||||
|
* Environment name (dev, staging, prod)
|
||||||
|
*/
|
||||||
|
environment: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database username (default: rentall_admin)
|
||||||
|
*/
|
||||||
|
dbUsername?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecretsStack extends cdk.Stack {
|
||||||
|
/**
|
||||||
|
* Database credentials secret
|
||||||
|
*/
|
||||||
|
public readonly databaseSecret: secretsmanager.Secret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Application secrets (JWT, etc.)
|
||||||
|
*/
|
||||||
|
public readonly appSecret: secretsmanager.Secret;
|
||||||
|
|
||||||
|
constructor(scope: Construct, id: string, props: SecretsStackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const { environment, dbUsername = "rentall_admin" } = props;
|
||||||
|
|
||||||
|
// Database credentials secret with auto-generated password
|
||||||
|
this.databaseSecret = new secretsmanager.Secret(this, "DatabaseSecret", {
|
||||||
|
secretName: `rentall/${environment}/database`,
|
||||||
|
description: `Database credentials for rentall ${environment} environment`,
|
||||||
|
generateSecretString: {
|
||||||
|
secretStringTemplate: JSON.stringify({
|
||||||
|
username: dbUsername,
|
||||||
|
}),
|
||||||
|
generateStringKey: "password",
|
||||||
|
excludePunctuation: true,
|
||||||
|
excludeCharacters: '/@"\'\\',
|
||||||
|
passwordLength: 32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Application secrets (JWT secret, etc.)
|
||||||
|
this.appSecret = new secretsmanager.Secret(this, "AppSecret", {
|
||||||
|
secretName: `rentall/${environment}/app`,
|
||||||
|
description: `Application secrets for rentall ${environment} environment`,
|
||||||
|
generateSecretString: {
|
||||||
|
secretStringTemplate: JSON.stringify({
|
||||||
|
// Add any additional app secrets here
|
||||||
|
}),
|
||||||
|
generateStringKey: "jwtSecret",
|
||||||
|
excludePunctuation: false,
|
||||||
|
passwordLength: 64,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
new cdk.CfnOutput(this, "DatabaseSecretArn", {
|
||||||
|
value: this.databaseSecret.secretArn,
|
||||||
|
description: "Database credentials secret ARN",
|
||||||
|
exportName: `DatabaseSecretArn-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "DatabaseSecretName", {
|
||||||
|
value: this.databaseSecret.secretName,
|
||||||
|
description: "Database credentials secret name",
|
||||||
|
exportName: `DatabaseSecretName-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "AppSecretArn", {
|
||||||
|
value: this.appSecret.secretArn,
|
||||||
|
description: "Application secrets ARN",
|
||||||
|
exportName: `AppSecretArn-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "AppSecretName", {
|
||||||
|
value: this.appSecret.secretName,
|
||||||
|
description: "Application secrets name",
|
||||||
|
exportName: `AppSecretName-${environment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user