infrastructure updates
This commit is contained in:
@@ -3,6 +3,7 @@ import "source-map-support/register";
|
|||||||
import * as cdk from "aws-cdk-lib";
|
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";
|
||||||
|
|
||||||
const app = new cdk.App();
|
const app = new cdk.App();
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ const envConfig: Record<
|
|||||||
frontendUrl: string;
|
frontendUrl: string;
|
||||||
sesFromEmail: string;
|
sesFromEmail: string;
|
||||||
emailEnabled: boolean;
|
emailEnabled: boolean;
|
||||||
|
natGateways: number;
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
staging: {
|
staging: {
|
||||||
@@ -27,6 +29,7 @@ const envConfig: Record<
|
|||||||
frontendUrl: "https://staging.villageshare.app",
|
frontendUrl: "https://staging.villageshare.app",
|
||||||
sesFromEmail: "noreply@villageshare.app",
|
sesFromEmail: "noreply@villageshare.app",
|
||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
|
natGateways: 1, // Single NAT gateway for cost optimization in staging
|
||||||
},
|
},
|
||||||
prod: {
|
prod: {
|
||||||
databaseUrl:
|
databaseUrl:
|
||||||
@@ -35,6 +38,7 @@ const envConfig: Record<
|
|||||||
frontendUrl: "https://villageshare.app",
|
frontendUrl: "https://villageshare.app",
|
||||||
sesFromEmail: "noreply@villageshare.app",
|
sesFromEmail: "noreply@villageshare.app",
|
||||||
emailEnabled: true,
|
emailEnabled: true,
|
||||||
|
natGateways: 2, // Multi-AZ NAT gateways for high availability in production
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -44,38 +48,71 @@ if (!config) {
|
|||||||
throw new Error(`Unknown environment: ${environment}`);
|
throw new Error(`Unknown environment: ${environment}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const envProps = {
|
||||||
|
env: {
|
||||||
|
account: process.env.CDK_DEFAULT_ACCOUNT,
|
||||||
|
region: process.env.CDK_DEFAULT_REGION || "us-east-1",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create the VPC stack first (other stacks depend on it)
|
||||||
|
const vpcStack = new VpcStack(app, `VpcStack-${environment}`, {
|
||||||
|
environment,
|
||||||
|
natGateways: config.natGateways,
|
||||||
|
maxAzs: 2,
|
||||||
|
...envProps,
|
||||||
|
description: `VPC infrastructure with private subnets and VPC endpoints (${environment})`,
|
||||||
|
tags: {
|
||||||
|
Environment: environment,
|
||||||
|
Project: "village-share",
|
||||||
|
Service: "networking",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
// Create the Condition Check Lambda stack
|
// Create the Condition Check Lambda stack
|
||||||
new ConditionCheckLambdaStack(app, `ConditionCheckLambdaStack-${environment}`, {
|
const conditionCheckStack = new ConditionCheckLambdaStack(
|
||||||
|
app,
|
||||||
|
`ConditionCheckLambdaStack-${environment}`,
|
||||||
|
{
|
||||||
environment,
|
environment,
|
||||||
databaseUrl: config.databaseUrl,
|
databaseUrl: config.databaseUrl,
|
||||||
frontendUrl: config.frontendUrl,
|
frontendUrl: config.frontendUrl,
|
||||||
sesFromEmail: config.sesFromEmail,
|
sesFromEmail: config.sesFromEmail,
|
||||||
emailEnabled: config.emailEnabled,
|
emailEnabled: config.emailEnabled,
|
||||||
env: {
|
vpc: vpcStack.vpc,
|
||||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
lambdaSecurityGroup: vpcStack.lambdaSecurityGroup,
|
||||||
region: process.env.CDK_DEFAULT_REGION || "us-east-1",
|
...envProps,
|
||||||
},
|
|
||||||
description: `Condition Check Reminder Lambda infrastructure (${environment})`,
|
description: `Condition Check Reminder Lambda infrastructure (${environment})`,
|
||||||
tags: {
|
tags: {
|
||||||
Environment: environment,
|
Environment: environment,
|
||||||
Project: "village-share",
|
Project: "village-share",
|
||||||
Service: "condition-check-reminder",
|
Service: "condition-check-reminder",
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add dependency on VPC stack
|
||||||
|
conditionCheckStack.addDependency(vpcStack);
|
||||||
|
|
||||||
// Create the Image Processor Lambda stack
|
// Create the Image Processor Lambda stack
|
||||||
new ImageProcessorLambdaStack(app, `ImageProcessorLambdaStack-${environment}`, {
|
const imageProcessorStack = new ImageProcessorLambdaStack(
|
||||||
|
app,
|
||||||
|
`ImageProcessorLambdaStack-${environment}`,
|
||||||
|
{
|
||||||
environment,
|
environment,
|
||||||
databaseUrl: config.databaseUrl,
|
databaseUrl: config.databaseUrl,
|
||||||
frontendUrl: config.frontendUrl,
|
frontendUrl: config.frontendUrl,
|
||||||
env: {
|
vpc: vpcStack.vpc,
|
||||||
account: process.env.CDK_DEFAULT_ACCOUNT,
|
lambdaSecurityGroup: vpcStack.lambdaSecurityGroup,
|
||||||
region: process.env.CDK_DEFAULT_REGION || "us-east-1",
|
...envProps,
|
||||||
},
|
|
||||||
description: `Image Processor Lambda infrastructure (${environment})`,
|
description: `Image Processor Lambda infrastructure (${environment})`,
|
||||||
tags: {
|
tags: {
|
||||||
Environment: environment,
|
Environment: environment,
|
||||||
Project: "village-share",
|
Project: "village-share",
|
||||||
Service: "image-processor",
|
Service: "image-processor",
|
||||||
},
|
},
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add dependency on VPC stack
|
||||||
|
imageProcessorStack.addDependency(vpcStack);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import * as lambda from "aws-cdk-lib/aws-lambda";
|
|||||||
import * as iam from "aws-cdk-lib/aws-iam";
|
import * as iam from "aws-cdk-lib/aws-iam";
|
||||||
import * as scheduler from "aws-cdk-lib/aws-scheduler";
|
import * as scheduler from "aws-cdk-lib/aws-scheduler";
|
||||||
import * as sqs from "aws-cdk-lib/aws-sqs";
|
import * as sqs from "aws-cdk-lib/aws-sqs";
|
||||||
|
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
||||||
import { Construct } from "constructs";
|
import { Construct } from "constructs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
@@ -36,6 +37,16 @@ interface ConditionCheckLambdaStackProps extends cdk.StackProps {
|
|||||||
* Whether emails are enabled
|
* Whether emails are enabled
|
||||||
*/
|
*/
|
||||||
emailEnabled?: boolean;
|
emailEnabled?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VPC for Lambda function (required for network isolation)
|
||||||
|
*/
|
||||||
|
vpc: ec2.IVpc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security group for Lambda function
|
||||||
|
*/
|
||||||
|
lambdaSecurityGroup: ec2.ISecurityGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConditionCheckLambdaStack extends cdk.Stack {
|
export class ConditionCheckLambdaStack extends cdk.Stack {
|
||||||
@@ -73,6 +84,8 @@ export class ConditionCheckLambdaStack extends cdk.Stack {
|
|||||||
sesFromEmail,
|
sesFromEmail,
|
||||||
sesFromName = "Village Share",
|
sesFromName = "Village Share",
|
||||||
emailEnabled = true,
|
emailEnabled = true,
|
||||||
|
vpc,
|
||||||
|
lambdaSecurityGroup,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
// Dead Letter Queue for failed Lambda invocations
|
// Dead Letter Queue for failed Lambda invocations
|
||||||
@@ -126,6 +139,13 @@ export class ConditionCheckLambdaStack extends cdk.Stack {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// VPC permissions - use AWS managed policy for Lambda VPC access
|
||||||
|
lambdaRole.addManagedPolicy(
|
||||||
|
iam.ManagedPolicy.fromAwsManagedPolicyName(
|
||||||
|
"service-role/AWSLambdaVPCAccessExecutionRole"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Lambda function
|
// Lambda function
|
||||||
this.lambdaFunction = new lambda.Function(
|
this.lambdaFunction = new lambda.Function(
|
||||||
this,
|
this,
|
||||||
@@ -171,6 +191,12 @@ export class ConditionCheckLambdaStack extends cdk.Stack {
|
|||||||
deadLetterQueue: this.deadLetterQueue,
|
deadLetterQueue: this.deadLetterQueue,
|
||||||
retryAttempts: 2,
|
retryAttempts: 2,
|
||||||
description: "Sends condition check reminder emails for rentals",
|
description: "Sends condition check reminder emails for rentals",
|
||||||
|
// VPC configuration for network isolation
|
||||||
|
vpc,
|
||||||
|
vpcSubnets: {
|
||||||
|
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
||||||
|
},
|
||||||
|
securityGroups: [lambdaSecurityGroup],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
|
|||||||
import * as s3 from "aws-cdk-lib/aws-s3";
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
||||||
import * as s3n from "aws-cdk-lib/aws-s3-notifications";
|
import * as s3n from "aws-cdk-lib/aws-s3-notifications";
|
||||||
import * as sqs from "aws-cdk-lib/aws-sqs";
|
import * as sqs from "aws-cdk-lib/aws-sqs";
|
||||||
|
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
||||||
import { Construct } from "constructs";
|
import { Construct } from "constructs";
|
||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
@@ -22,6 +23,16 @@ interface ImageProcessorLambdaStackProps extends cdk.StackProps {
|
|||||||
* Frontend URL for CORS configuration
|
* Frontend URL for CORS configuration
|
||||||
*/
|
*/
|
||||||
frontendUrl: string;
|
frontendUrl: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* VPC for Lambda function (required for network isolation)
|
||||||
|
*/
|
||||||
|
vpc: ec2.IVpc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security group for Lambda function
|
||||||
|
*/
|
||||||
|
lambdaSecurityGroup: ec2.ISecurityGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ImageProcessorLambdaStack extends cdk.Stack {
|
export class ImageProcessorLambdaStack extends cdk.Stack {
|
||||||
@@ -47,7 +58,7 @@ export class ImageProcessorLambdaStack extends cdk.Stack {
|
|||||||
) {
|
) {
|
||||||
super(scope, id, props);
|
super(scope, id, props);
|
||||||
|
|
||||||
const { environment, databaseUrl, frontendUrl } = props;
|
const { environment, databaseUrl, frontendUrl, vpc, lambdaSecurityGroup } = props;
|
||||||
|
|
||||||
// Dead Letter Queue for failed Lambda invocations
|
// Dead Letter Queue for failed Lambda invocations
|
||||||
this.deadLetterQueue = new sqs.Queue(this, "ImageProcessorDLQ", {
|
this.deadLetterQueue = new sqs.Queue(this, "ImageProcessorDLQ", {
|
||||||
@@ -143,6 +154,13 @@ export class ImageProcessorLambdaStack extends cdk.Stack {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// VPC permissions - use AWS managed policy for Lambda VPC access
|
||||||
|
lambdaRole.addManagedPolicy(
|
||||||
|
iam.ManagedPolicy.fromAwsManagedPolicyName(
|
||||||
|
"service-role/AWSLambdaVPCAccessExecutionRole"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
// Lambda function
|
// Lambda function
|
||||||
this.lambdaFunction = new lambda.Function(this, "ImageProcessorLambda", {
|
this.lambdaFunction = new lambda.Function(this, "ImageProcessorLambda", {
|
||||||
functionName: `image-processor-${environment}`,
|
functionName: `image-processor-${environment}`,
|
||||||
@@ -183,6 +201,12 @@ export class ImageProcessorLambdaStack extends cdk.Stack {
|
|||||||
retryAttempts: 2,
|
retryAttempts: 2,
|
||||||
description:
|
description:
|
||||||
"Processes uploaded images: extracts metadata and strips EXIF",
|
"Processes uploaded images: extracts metadata and strips EXIF",
|
||||||
|
// VPC configuration for network isolation
|
||||||
|
vpc,
|
||||||
|
vpcSubnets: {
|
||||||
|
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
||||||
|
},
|
||||||
|
securityGroups: [lambdaSecurityGroup],
|
||||||
});
|
});
|
||||||
|
|
||||||
// S3 event notification for staging uploads
|
// S3 event notification for staging uploads
|
||||||
|
|||||||
176
infrastructure/cdk/lib/vpc-stack.ts
Normal file
176
infrastructure/cdk/lib/vpc-stack.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import * as cdk from "aws-cdk-lib";
|
||||||
|
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
||||||
|
import { Construct } from "constructs";
|
||||||
|
|
||||||
|
interface VpcStackProps extends cdk.StackProps {
|
||||||
|
/**
|
||||||
|
* Environment name (staging, prod)
|
||||||
|
*/
|
||||||
|
environment: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of AZs to use (default: 2)
|
||||||
|
*/
|
||||||
|
maxAzs?: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of NAT Gateways (default: 1 for cost optimization)
|
||||||
|
* Use 2 for high availability in production
|
||||||
|
*/
|
||||||
|
natGateways?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class VpcStack extends cdk.Stack {
|
||||||
|
/**
|
||||||
|
* The VPC created by this stack
|
||||||
|
*/
|
||||||
|
public readonly vpc: ec2.Vpc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Security group for Lambda functions
|
||||||
|
*/
|
||||||
|
public readonly lambdaSecurityGroup: ec2.SecurityGroup;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* S3 Gateway endpoint (free)
|
||||||
|
*/
|
||||||
|
public readonly s3Endpoint: ec2.GatewayVpcEndpoint;
|
||||||
|
|
||||||
|
constructor(scope: Construct, id: string, props: VpcStackProps) {
|
||||||
|
super(scope, id, props);
|
||||||
|
|
||||||
|
const { environment, maxAzs = 2, natGateways = 1 } = props;
|
||||||
|
|
||||||
|
// Create VPC with public and private subnets
|
||||||
|
this.vpc = new ec2.Vpc(this, "VillageShareVpc", {
|
||||||
|
vpcName: `village-share-vpc-${environment}`,
|
||||||
|
ipAddresses: ec2.IpAddresses.cidr("10.0.0.0/16"),
|
||||||
|
maxAzs,
|
||||||
|
natGateways,
|
||||||
|
subnetConfiguration: [
|
||||||
|
{
|
||||||
|
name: "Public",
|
||||||
|
subnetType: ec2.SubnetType.PUBLIC,
|
||||||
|
cidrMask: 24,
|
||||||
|
mapPublicIpOnLaunch: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Private",
|
||||||
|
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
|
||||||
|
cidrMask: 24,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Isolated",
|
||||||
|
subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
|
||||||
|
cidrMask: 24,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Enable DNS support for VPC endpoints
|
||||||
|
enableDnsHostnames: true,
|
||||||
|
enableDnsSupport: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Security group for Lambda functions
|
||||||
|
this.lambdaSecurityGroup = new ec2.SecurityGroup(
|
||||||
|
this,
|
||||||
|
"LambdaSecurityGroup",
|
||||||
|
{
|
||||||
|
vpc: this.vpc,
|
||||||
|
securityGroupName: `lambda-sg-${environment}`,
|
||||||
|
description: "Security group for Lambda functions in VPC",
|
||||||
|
allowAllOutbound: true, // Lambda needs outbound for AWS services
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Security group for VPC endpoints
|
||||||
|
const vpcEndpointSecurityGroup = new ec2.SecurityGroup(
|
||||||
|
this,
|
||||||
|
"VpcEndpointSecurityGroup",
|
||||||
|
{
|
||||||
|
vpc: this.vpc,
|
||||||
|
securityGroupName: `vpc-endpoint-sg-${environment}`,
|
||||||
|
description: "Security group for VPC Interface Endpoints",
|
||||||
|
allowAllOutbound: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Allow HTTPS traffic from Lambda security group to VPC endpoints
|
||||||
|
vpcEndpointSecurityGroup.addIngressRule(
|
||||||
|
this.lambdaSecurityGroup,
|
||||||
|
ec2.Port.tcp(443),
|
||||||
|
"Allow HTTPS from Lambda functions"
|
||||||
|
);
|
||||||
|
|
||||||
|
// S3 Gateway Endpoint (FREE - no NAT charges for S3 traffic)
|
||||||
|
this.s3Endpoint = this.vpc.addGatewayEndpoint("S3Endpoint", {
|
||||||
|
service: ec2.GatewayVpcEndpointAwsService.S3,
|
||||||
|
subnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
|
||||||
|
});
|
||||||
|
|
||||||
|
// SES Interface Endpoint (for sending emails without NAT)
|
||||||
|
this.vpc.addInterfaceEndpoint("SesEndpoint", {
|
||||||
|
service: ec2.InterfaceVpcEndpointAwsService.SES,
|
||||||
|
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
|
||||||
|
securityGroups: [vpcEndpointSecurityGroup],
|
||||||
|
privateDnsEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// SQS Interface Endpoint (for DLQ access)
|
||||||
|
this.vpc.addInterfaceEndpoint("SqsEndpoint", {
|
||||||
|
service: ec2.InterfaceVpcEndpointAwsService.SQS,
|
||||||
|
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
|
||||||
|
securityGroups: [vpcEndpointSecurityGroup],
|
||||||
|
privateDnsEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// CloudWatch Logs Interface Endpoint
|
||||||
|
this.vpc.addInterfaceEndpoint("CloudWatchLogsEndpoint", {
|
||||||
|
service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS,
|
||||||
|
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
|
||||||
|
securityGroups: [vpcEndpointSecurityGroup],
|
||||||
|
privateDnsEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scheduler Interface Endpoint (for EventBridge Scheduler)
|
||||||
|
// Note: EventBridge Scheduler uses the scheduler.{region}.amazonaws.com endpoint
|
||||||
|
this.vpc.addInterfaceEndpoint("SchedulerEndpoint", {
|
||||||
|
service: new ec2.InterfaceVpcEndpointService(
|
||||||
|
`com.amazonaws.${cdk.Stack.of(this).region}.scheduler`
|
||||||
|
),
|
||||||
|
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
|
||||||
|
securityGroups: [vpcEndpointSecurityGroup],
|
||||||
|
privateDnsEnabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add tags to subnets for easy identification
|
||||||
|
cdk.Tags.of(this.vpc).add("Environment", environment);
|
||||||
|
cdk.Tags.of(this.vpc).add("Project", "village-share");
|
||||||
|
|
||||||
|
// Outputs
|
||||||
|
new cdk.CfnOutput(this, "VpcId", {
|
||||||
|
value: this.vpc.vpcId,
|
||||||
|
description: "VPC ID",
|
||||||
|
exportName: `VpcId-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "VpcCidr", {
|
||||||
|
value: this.vpc.vpcCidrBlock,
|
||||||
|
description: "VPC CIDR block",
|
||||||
|
exportName: `VpcCidr-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "PrivateSubnetIds", {
|
||||||
|
value: this.vpc
|
||||||
|
.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS })
|
||||||
|
.subnetIds.join(","),
|
||||||
|
description: "Private subnet IDs",
|
||||||
|
exportName: `PrivateSubnetIds-${environment}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
new cdk.CfnOutput(this, "LambdaSecurityGroupId", {
|
||||||
|
value: this.lambdaSecurityGroup.securityGroupId,
|
||||||
|
description: "Security group ID for Lambda functions",
|
||||||
|
exportName: `LambdaSecurityGroupId-${environment}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user