infrastructure with aws cdk

This commit is contained in:
jackiettran
2026-01-21 14:18:07 -05:00
parent 28554acc2d
commit 0136b74ee0
6 changed files with 1049 additions and 20 deletions

View 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",
});
}
}

View 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}`,
});
}
}

View 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",
});
}
}

View 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}`,
});
}
}

View 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}`,
});
}
}