Feat/customize subnet (#7)

* update: customize subnets

* update: doc

* fix: bad constant

---------

Co-authored-by: GareArc <chen4851@purude.edu>
This commit is contained in:
Xiyuan Chen
2024-08-28 21:05:02 -04:00
committed by GitHub
parent 6c8a438257
commit e13ade1123
12 changed files with 214 additions and 34 deletions
+9
View File
@@ -8,6 +8,15 @@ CDK_DEFAULT_ACCOUNT=
# Deploy Dify Enterprise in existing VPC. If not set, CDK will create a new VPC for you.
DEPLOY_VPC_ID=
# Subnet IDs for Dify Enterprise deployment
#
# e.g. EKS_CLUSTER_SUBNETS=subnet-1,subnet-2,subnet-3
EKS_CLUSTER_SUBNETS=
EKS_NODES_SUBNETS=
REDIS_SUBNETS=
RDS_SUBNETS=
OPENSEARCH_SUBNETS=
# AWS EKS Helm chart repository URL (Version 1.8.1)
# Set this ONLY if you are using AWS China regions. Please contact us for assistance.
# For more information, visit: https://github.com/aws/eks-charts
+6
View File
@@ -60,6 +60,12 @@ Deploy Dify Enterprise on AWS using CDK.
- `CDK_DEFAULT_REGION`: The AWS region where Dify Enterprise will be deployed.
- `CDK_DEFAULT_ACCOUNT`: Your AWS account ID.
- `DEPLOY_VPC_ID`: The ID of an existing VPC for deployment. If not set, CDK will create one for you.
- Subnets Configuration (`DEPLOY_VPC_ID` required, comma-separated without spaces):
- `EKS_CLUSTER_SUBNETS`: Subnet IDs for the EKS control plane. Requires at least 2 subnets in different Availability Zones (AZs).
- `EKS_NODES_SUBNETS`: Subnet IDs for the EKS worker nodes. Requires at least 2 subnets in different AZs.
- `REDIS_SUBNETS`: Subnet IDs for Redis deployment.
- `RDS_SUBNETS`: subnet ids for RDS database. (At least 2 with different AZs)
- `OPENSEARCH_SUBNETS`: Subnet IDs for OpenSearch deployment.
- `AWS_EKS_CHART_REPO_URL`: (For AWS China regions ONLY) The AWS EKS Helm chart repository URL.
- `RDS_PUBLIC_ACCESSIBLE`: Set to `true` to make RDS publicly accessible (NOT RECOMMENDED).
+2
View File
@@ -15,6 +15,7 @@ interface managedNodeGroups {
maxSize: number, // maximum number of nodes
instanceType: InstanceType, // instance type for the nodes
diskSize: number, // disk size for the nodes
workerNodeSubnetIds: string[]; // subnets for the EKS cluster worker nodes
}
}
@@ -22,4 +23,5 @@ export interface EksClusterConfig {
version: KubernetesVersion; // version of the EKS cluster
tags: { [key: string]: string }; // tags for the EKS cluster
managedNodeGroups: managedNodeGroups; // managed node groups for the EKS cluster
vpcSubnetIds: string[]; // subnets for the EKS cluster control plane
}
+5 -1
View File
@@ -1,12 +1,16 @@
export interface OpenSearchConfig {
enabled: boolean, // Whether to enable OpenSearch
multiAz: {
enabled: boolean, // Whether to enable multi-AZ
azCount: 2 | 3, // The number of availability zones to deploy the OpenSearch domain
}
subnetIds: string[], // The subnet IDs to deploy the OpenSearch domain
capacity: {
dataNodes: number | undefined, // The number of data nodes
dataNodeInstanceType: string | undefined, // The instance type of the data nodes, see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html
masterNodes?: number | undefined, // The number of master nodes
masterNodeInstanceType?: string | undefined, // The instance type of the master nodes, see https://docs.aws.amazon.com/opensearch-service/latest/developerguide/supported-instance-types.html
multiAzWithStandbyEnabled?: boolean,
}
dataNodeSize: number | undefined // The storage size of the data nodes, in GB
}
+5
View File
@@ -9,4 +9,9 @@ export interface PostgresSQLConfig {
dbCredentialUsername: string; // username for the db, password will be generated and stored in Secrets Manager
removeWhenDestroyed: boolean; // whether to remove the cluster when the stack is destroyed
backupRetention: number; // number of days to retain backups, 0 to disable backups
subnetIds?: string[]; // list of subnet IDs to deploy the db in
multiAz: {
enabled: boolean; // whether to deploy the db in multiple AZs
subnetGroupName?: string; // name of the subnet group to deploy the db in
}
}
+5 -1
View File
@@ -1,7 +1,11 @@
export interface RedisConfig {
nodeType: string; // node type for the Redis cluster
multiAZ: boolean; // whether to enable multi-AZ
subnetIds: string[]; // list of subnet IDs
multiAZ: {
enabled: boolean; // whether to enable multi-AZ
subnetGroupName: string; // name of the subnet group
}
parameterGroup: string; // name of the parameter group
engineVersion: string; // version of the engine
readReplicas: number; // number of read replicas
+27 -6
View File
@@ -1,7 +1,7 @@
import { InstanceType } from "aws-cdk-lib/aws-ec2";
import { KubernetesVersion } from "aws-cdk-lib/aws-eks";
import { PostgresEngineVersion } from "aws-cdk-lib/aws-rds";
import { EC2_INSTANCE_MAP, RDS_INSTANCE_MAP, REDIS_NODE_MAP } from "./constants";
import { DESTROY_WHEN_REMOVE, EC2_INSTANCE_MAP, RDS_INSTANCE_MAP, REDIS_NODE_MAP } from "./constants";
import { StackConfig } from "./stackConfig";
export interface ProdStackConfig extends StackConfig {
@@ -14,8 +14,11 @@ export const prodConfig: ProdStackConfig = {
account: process.env.CDK_PROD_ACCOUNT || process.env.CDK_DEFAULT_ACCOUNT || '',
cluster: {
version: KubernetesVersion.V1_29,
version: KubernetesVersion.V1_30,
tags: { "marketplace": "dify" },
// at least 2 ids
// [],
vpcSubnetIds: process.env.EKS_CLUSTER_SUBNETS?.split(',') || [],
managedNodeGroups: {
app: {
desiredSize: 3,
@@ -23,12 +26,14 @@ export const prodConfig: ProdStackConfig = {
maxSize: 6,
instanceType: new InstanceType(EC2_INSTANCE_MAP['8c32m']),
diskSize: 100,
// at least 2 ids
workerNodeSubnetIds: process.env.EKS_NODES_SUBNETS?.split(',') || []
}
},
},
s3: {
removeWhenDestroyed: false,
removeWhenDestroyed: DESTROY_WHEN_REMOVE || false,
},
postgresSQL: {
@@ -38,7 +43,13 @@ export const prodConfig: ProdStackConfig = {
dbCredentialUsername: 'clusteradmin',
backupRetention: 0,
storageSize: 512,
removeWhenDestroyed: false,
removeWhenDestroyed: DESTROY_WHEN_REMOVE,
// at least 2 ids
subnetIds: process.env.RDS_SUBNETS?.split(',') || [],
multiAz: {
enabled: false,
subnetGroupName: ''
}
},
redis: {
@@ -46,15 +57,25 @@ export const prodConfig: ProdStackConfig = {
parameterGroup: "default.redis6.x",
nodeType: REDIS_NODE_MAP['12.93m'],
readReplicas: 1,
multiAZ: true
subnetIds: process.env.REDIS_SUBNETS?.split(',') || [],
multiAZ: {
enabled: false,
subnetGroupName: ''
},
},
openSearch: {
enabled: true,
multiAz: {
enabled: false,
azCount: 2
},
subnetIds: process.env.OPENSEARCH_SUBNETS?.split(',') || [],
capacity: {
dataNodes: 2,
dataNodeInstanceType: 'r6g.large.search',
multiAzWithStandbyEnabled: true,
// masterNodes: 2,
// masterNodeInstanceType: 'r6g.xlarge.search'
},
dataNodeSize: 100
}
+17 -2
View File
@@ -20,6 +20,7 @@ export const testConfig: TestStackConfig = {
cluster: {
version: KubernetesVersion.V1_29,
tags: { "marketplace": "dify" },
vpcSubnetIds: [],
managedNodeGroups: {
app: {
desiredSize: 1,
@@ -27,6 +28,7 @@ export const testConfig: TestStackConfig = {
maxSize: 1,
instanceType: new InstanceType(EC2_INSTANCE_MAP['4c16m']),
diskSize: 100,
workerNodeSubnetIds: []
}
},
},
@@ -43,6 +45,11 @@ export const testConfig: TestStackConfig = {
backupRetention: 0,
storageSize: 256,
removeWhenDestroyed: true,
subnetIds: [],
multiAz: {
enabled: false,
subnetGroupName: ''
}
},
redis: {
@@ -50,15 +57,23 @@ export const testConfig: TestStackConfig = {
parameterGroup: "default.redis6.x",
nodeType: REDIS_NODE_MAP['6.38m'],
readReplicas: 1,
multiAZ: true
subnetIds: [],
multiAZ: {
enabled: false,
subnetGroupName: ''
}
},
openSearch: {
enabled: true,
multiAz: {
enabled: false,
azCount: 2
},
subnetIds: [],
capacity: {
dataNodes: 2,
dataNodeInstanceType: 'r6g.large.search',
multiAzWithStandbyEnabled: true,
},
dataNodeSize: 100
}
+23 -1
View File
@@ -1,5 +1,6 @@
import * as blueprints from '@aws-quickstart/eks-blueprints';
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { IVpc, SubnetType } from 'aws-cdk-lib/aws-ec2';
import * as eks from 'aws-cdk-lib/aws-eks';
import { Construct } from 'constructs';
@@ -22,12 +23,14 @@ export class DifyStackConstruct {
scope: Construct;
difyProps: DifyStackProps;
props?: cdk.StackProps;
vpc: IVpc;
constructor(scope: Construct, id: string, difyProps: DifyStackProps, props: cdk.StackProps) {
this.scope = scope;
this.id = id;
this.difyProps = difyProps;
this.props = props;
this.vpc = difyProps.vpc;
}
build() {
const scope = this.scope;
@@ -35,9 +38,28 @@ export class DifyStackConstruct {
const difyProps = this.difyProps;
const config = difyProps.config;
const eksSubnets: ec2.SubnetSelection[] = config.cluster.vpcSubnetIds && config.cluster.vpcSubnetIds.length > 0
? config.cluster.vpcSubnetIds.map(subnetId => ({
subnets: [ec2.Subnet.fromSubnetId(this.vpc, `${id}-VpcSubnet-${subnetId}`, subnetId)],
}))
: [
{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
];
console.log(`EKS: using subnets: ${eksSubnets.map(subnet => subnet.subnets?.map(subnet => subnet.subnetId).join(', '))}`);
const appNodeSubnets: ec2.SubnetSelection = {
subnets: config.cluster.managedNodeGroups.app.workerNodeSubnetIds && config.cluster.managedNodeGroups.app.workerNodeSubnetIds.length > 0
? config.cluster.managedNodeGroups.app.workerNodeSubnetIds.map(subnetId => ec2.Subnet.fromSubnetId(this.vpc, `${id}-WorkerNodeSubnet-${subnetId}`, subnetId))
: difyProps.vpc.selectSubnets({ subnetType: SubnetType.PRIVATE_WITH_EGRESS }).subnets
}
console.log(`Worker Nodes: using subnets: ${appNodeSubnets.subnets?.map(subnet => subnet.subnetId).join(', ')}`);
const clusterProvider = new blueprints.GenericClusterProvider({
version: config.cluster.version,
tags: config.cluster.tags,
vpcSubnets: eksSubnets,
managedNodeGroups: [
{ // worker node group for app
id: 'app',
@@ -46,7 +68,7 @@ export class DifyStackConstruct {
maxSize: config.cluster.managedNodeGroups.app.maxSize,
instanceTypes: [config.cluster.managedNodeGroups.app.instanceType],
amiType: eks.NodegroupAmiType.AL2_ARM_64,
nodeGroupSubnets: { subnetType: SubnetType.PRIVATE_WITH_EGRESS },
nodeGroupSubnets: appNodeSubnets,
enableSsmPermissions: true,
tags: {
"Name": cdk.Aws.STACK_NAME + "-Workload",
+42 -14
View File
@@ -11,7 +11,6 @@ interface OpenSearchProps {
vpc: ec2.IVpc,
}
export class OpensearchResourceProvider implements blueprints.ResourceProvider<opensearch.IDomain> {
private readonly config: StackConfig;
private readonly vpc: ec2.IVpc;
@@ -22,27 +21,56 @@ export class OpensearchResourceProvider implements blueprints.ResourceProvider<o
}
provide(context: blueprints.ResourceContext): opensearch.IDomain {
const selectedSubnet = this.vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
}).subnets.slice(0, 2);
const { multiAz, subnetIds, capacity, dataNodeSize } = this.config.openSearch;
const domain = new opensearch.Domain(context.scope, `${getConstructPrefix(this.config)}-OpensearchDomain`, {
let selectedSubnets: ec2.ISubnet[];
if (multiAz.enabled) {
// If multi-AZ is enabled, use either user-provided subnet IDs or private subnets of specified availabilityZoneCount
selectedSubnets = subnetIds && subnetIds.length > 0
? subnetIds.map(id => ec2.Subnet.fromSubnetId(this.vpc, `Subnet-${id}`, id))
: this.vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
}).subnets.slice(0, multiAz.azCount);
if (selectedSubnets.length !== multiAz.azCount) {
throw new Error(`The number of provided subnets (${selectedSubnets.length}) does not match the required availability zones (${multiAz.azCount}).`);
}
console.log(`OpenSearch: using subnets: ${selectedSubnets.map(subnet => subnet.subnetId).join(', ')}`);
} else {
// If multi-AZ is not enabled, use the first available subnet or a user-provided subnet
selectedSubnets = subnetIds && subnetIds.length > 0
? [ec2.Subnet.fromSubnetId(this.vpc, `Subnet-${subnetIds[0]}`, subnetIds[0])]
: this.vpc.selectSubnets({
subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS
}).subnets.slice(0, 1);
console.log(`OpenSearch: using subnet: ${selectedSubnets[0].subnetId}`);
}
const domainProps: opensearch.DomainProps = {
version: opensearch.EngineVersion.OPENSEARCH_2_13,
vpcSubnets: [{ subnets: selectedSubnet }],
removalPolicy: DESTROY_WHEN_REMOVE ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN,
capacity: this.config.openSearch.capacity,
zoneAwareness: {
enabled: true,
availabilityZoneCount: 2,
vpcSubnets: [{ subnets: selectedSubnets }],
capacity: {
...capacity,
multiAzWithStandbyEnabled: multiAz.enabled,
},
ebs: {
volumeSize: this.config.openSearch.dataNodeSize,
volumeSize: dataNodeSize,
volumeType: ec2.EbsDeviceVolumeType.GP3,
throughput: 125,
iops: 3000,
},
vpc: this.vpc
});
return domain
zoneAwareness: {
enabled: multiAz.enabled,
availabilityZoneCount: multiAz.azCount,
},
vpc: this.vpc,
};
const domain = new opensearch.Domain(context.scope, `${getConstructPrefix(this.config)}-OpensearchDomain`, domainProps);
return domain;
}
}
+44 -3
View File
@@ -49,6 +49,48 @@ export class PostgresSQLResourceProvider implements ResourceProvider<rds.Databas
);
}
let vpcSubnets: ec2.SubnetSelection;
const multiAz = this.config.postgresSQL.multiAz;
if (multiAz.enabled) {
// Multi-AZ deployment: Use either user-defined subnets or private subnets across multiple AZs
if (this.config.postgresSQL.subnetIds && this.config.postgresSQL.subnetIds.length > 0) {
const selectedSubnets = this.config.postgresSQL.subnetIds.map(id =>
ec2.Subnet.fromSubnetId(this.vpc, `Subnet-${id}`, id)
);
vpcSubnets = { subnets: selectedSubnets };
console.log(`PostgresSQL: using subnets: ${selectedSubnets.map(subnet => subnet.subnetId).join(', ')}`);
} else if (multiAz.subnetGroupName) {
// Use a specific subnet group for multi-AZ
vpcSubnets = { subnetGroupName: multiAz.subnetGroupName };
console.log(`PostgresSQL: using subnet group: ${multiAz.subnetGroupName}`);
} else {
// Fallback to default behavior
vpcSubnets = {
subnetType: publiclyAccessible ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS,
};
console.log(`PostgresSQL: using default subnets (${publiclyAccessible ? 'public' : 'private'})`);
}
} else {
// Single-AZ deployment: Use the first two available subnets or a user-provided subnets
if (this.config.postgresSQL.subnetIds && this.config.postgresSQL.subnetIds.length > 0) {
const subnets = this.config.postgresSQL.subnetIds.slice(0, 2);
vpcSubnets = {
subnets: subnets.map(id => ec2.Subnet.fromSubnetId(this.vpc, `Subnet-${id}`, id)),
};
console.log(`PostgresSQL: using subnets: ${subnets.join(', ')}`);
} else {
vpcSubnets = {
subnetType: publiclyAccessible ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS,
};
console.log(`PostgresSQL: using default subnets (${publiclyAccessible ? 'public' : 'private'})`);
}
}
const postgresInstance = new rds.DatabaseInstance(context.scope, `${getConstructPrefix(this.config)}-PostgresRDSInstance`, {
engine: rds.DatabaseInstanceEngine.postgres({
version: this.config.postgresSQL.version,
@@ -63,9 +105,8 @@ export class PostgresSQLResourceProvider implements ResourceProvider<rds.Databas
storageType: rds.StorageType.GP2,
removalPolicy: this.config.postgresSQL.removeWhenDestroyed ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN,
publiclyAccessible: publiclyAccessible,
vpcSubnets: {
subnetType: publiclyAccessible ? ec2.SubnetType.PUBLIC : ec2.SubnetType.PRIVATE_WITH_EGRESS,
},
vpcSubnets: vpcSubnets,
multiAz: multiAz.enabled,
backupRetention: cdk.Duration.days(this.config.postgresSQL.backupRetention),
});
+29 -6
View File
@@ -38,21 +38,44 @@ export class RedisResourceProvider implements ResourceProvider<elasticache.CfnRe
"Allow access to Redis from the VPC"
)
let subnets: string[] = [];
this.vpc.privateSubnets.forEach(function (value) {
subnets.push(value.subnetId);
});
let subnetIds: string[];
const multiAz = this.config.redis.multiAZ;
if (multiAz.enabled) {
// Multi-AZ deployment: Use either user-defined subnets or private subnets across multiple AZs
if (this.config.redis.subnetIds && this.config.redis.subnetIds.length > 0) {
subnetIds = this.config.redis.subnetIds;
console.log(`Redis: using subnets: ${subnetIds.join(', ')}`);
} else if (this.config.redis.multiAZ.subnetGroupName) {
// Use a specific subnet group name if provided (Note: This is a name, not IDs)
subnetIds = this.vpc.selectSubnets({ subnetGroupName: this.config.redis.multiAZ.subnetGroupName }).subnetIds;
console.log(`Redis: using subnetGroupName: ${this.config.redis.multiAZ.subnetGroupName}`);
} else {
// Fallback to default private subnets for multi-AZ
subnetIds = this.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds;
console.log(`Redis: using default private subnets: ${subnetIds.join(', ')}`);
}
} else {
// Single-AZ deployment: Use the first available subnet or a user-provided subnet
if (this.config.redis.subnetIds && this.config.redis.subnetIds.length > 0) {
subnetIds = [this.config.redis.subnetIds[0]];
console.log(`Redis: using subnet: ${subnetIds[0]}`);
} else {
subnetIds = [this.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds[0]];
console.log(`Redis: using default private subnet: ${subnetIds[0]}`);
}
}
const ecSubnetGroup = new elasticache.CfnSubnetGroup(context.scope, `${getConstructPrefix(this.config)}-RedisSubnetGroup`, {
description: 'Redis Subnet Group',
subnetIds: subnets,
subnetIds: subnetIds,
cacheSubnetGroupName: `${getConstructPrefix(this.config)}-RedisSubnetGroup`,
});
const redis_cluster = new elasticache.CfnReplicationGroup(context.scope, `${getConstructPrefix(this.config)}-Redis`, {
cacheNodeType: this.config.redis.nodeType,
engine: 'Redis',
multiAzEnabled: this.config.redis.multiAZ,
multiAzEnabled: multiAz.enabled,
cacheParameterGroupName: this.config.redis.parameterGroup,
engineVersion: this.config.redis.engineVersion,
cacheSubnetGroupName: ecSubnetGroup.cacheSubnetGroupName!,