AWS CDK中Fargate与RDS间非明显循环依赖排查与解决
问题场景
逻辑上仅存在PlausibleStack(Fargate任务)依赖RDSStack(RDS实例)的单向关系,但CDK部署时出现循环依赖错误:
Error: 'RDSStack' depends on 'PlausibleStack' (RDSStack -> PlausibleStack/PlausibleSecurityGroup/Resource.GroupId). Adding this dependency (PlausibleStack -> RDSStack/MainRDS/Resource.DbiResourceId) would create a cyclic reference.
代码结构如下:
bin.ts
const vpc = new VPCStack(app, "VPC", { deployEnv: environment, env: { region: ConstValues.region, account: ConstValues.account[environment], }, }) const bastion = new BastionStack(app, "Bastion", { deployEnv: environment, vpc: vpc.vpc, env: { region: ConstValues.region, account: ConstValues.account[environment], }, }) const rds = new RDSStack(app, "RDSStack", { deployEnv: environment, vpc: vpc.vpc, bastion: bastion.bastionHost, env: { region: ConstValues.region, account: ConstValues.account[environment], }, }) new PlausibleStack(app, "PlausibleStack", { deployEnv: environment, rds: rds.rds, vpc: vpc.vpc, env: { region: ConstValues.region, account: ConstValues.account[environment], }, })
rds-stack.ts
type RDSStackProps = { vpc: ec2.Vpc; bastion: ec2.BastionHostLinux; } & StackProps; export class RDSStack extends Stack { public rds: rds.DatabaseInstance; private vpc: ec2.Vpc; private deployEnv: DeployEnvironments; constructor(scope: Construct, id: string, props: RDSStackProps) { super(scope, id, props); this.vpc = props.vpc; this.deployEnv = props.deployEnv; const credsSecretName = `/${id}/rds/creds/main-rds`.toLowerCase() const creds = new rds.DatabaseSecret(this, 'MainRDSCredentials', { secretName: credsSecretName, username: 'postgres' }) const engine = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_14 }); const instanceType = isDev(props.deployEnv) ? ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MICRO) : ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.SMALL) this.rds = new rds.DatabaseInstance(this, 'MainRDS', { vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_ISOLATED, }, vpc: props.vpc, removalPolicy: isDev(props.deployEnv) ? RemovalPolicy.DESTROY : RemovalPolicy.SNAPSHOT, backupRetention: isProd(props.deployEnv) ? Duration.days(1) : Duration.days(0), credentials: rds.Credentials.fromSecret(creds), databaseName: ConstValues.database.backend.name, engine: engine, instanceType: instanceType, allocatedStorage: 10, }) this.rds.connections.allowDefaultPortFrom(props.bastion) this.initCustomResource('RDSInit', creds); } }
plausible-stack.ts
type PlausibleStackProps = { vpc: ec2.Vpc; rds: DatabaseInstance } & StackProps; export class PlausibleStack extends cdk.Stack { private vpc: ec2.Vpc; private deployEnv: DeployEnvironments; private cluster: ecs.Cluster; private rds: DatabaseInstance; constructor(scope: Construct, id: string, props: PlausibleStackProps) { super(scope, id, props); this.deployEnv = props.deployEnv; this.vpc = props.vpc; this.rds = props.rds; this.cluster = new ecs.Cluster(this, 'PlausibleCluster', { vpc: this.vpc, }) this.setupPlausible(); } private setupPlausible() { const securityGroup = new ec2.SecurityGroup(this, 'PlausibleSecurityGroup', { vpc: this.vpc, }) const taskDef = new ecs.FargateTaskDefinition(this, 'PlausibleTask', { memoryLimitMiB: 1024, cpu: 512 }); const service = new ecs.FargateService(this, 'PlausibleService', { cluster: this.cluster, taskDefinition: taskDef, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }, desiredCount: 1, securityGroups: [securityGroup] }); const lb = new elbv2.ApplicationLoadBalancer(this, 'PlausibleALB', { vpc: this.vpc, internetFacing: true }); const listener = lb.addListener('PlausibleALBListener', { port: 80 }); const plausibleContainer = taskDef.addContainer('Plausible', { image: ecs.ContainerImage.fromRegistry('plausible/analytics:v2.0'), logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'plausible' }), environment: { BASE_URL: `https://${lb.loadBalancerDnsName}`, DATABASE_URL: `postgres://${ConstValues.database.analytics.user}@${this.rds.instanceEndpoint}/${ConstValues.database.analytics.name}`, } }); plausibleContainer.addPortMappings({ containerPort: 8000, }); listener.addTargets('PlausibleALBTargetPlausibleService', { port: 8000, targets: [service], }); this.rds.grantConnect(taskDef.taskRole, ConstValues.database.analytics.user); this.rds.connections.allowDefaultPortFrom(securityGroup); } }
成因分析
问题核心出在this.rds.connections.allowDefaultPortFrom(securityGroup)这一行:
- 调用该方法时,CDK会修改RDS实例的安全组,添加允许Plausible安全组访问的入站规则。由于RDS和Plausible分属不同栈,RDS栈需要依赖Plausible栈的安全组资源(获取其GroupId),形成
RDSStack → PlausibleStack的依赖。 - 同时,Plausible栈中使用了
this.rds.instanceEndpoint和this.rds.grantConnect,形成PlausibleStack → RDSStack的依赖。 - 双向依赖触发CDK的循环依赖检测,抛出错误。
grantConnect本身是单向依赖(Plausible依赖RDS),不会引发循环,问题根源是跨栈修改RDS安全组的操作。
解决方法
最简洁的修复方式是反转规则添加方向:不在RDS安全组中添加入站规则,而是在Plausible的安全组中添加允许出站访问RDS的规则,仅保留PlausibleStack → RDSStack的单向依赖,打破循环。
修改plausible-stack.ts中的代码:
// 移除原有的入站规则添加代码 // this.rds.connections.allowDefaultPortFrom(securityGroup); // 替换为出站规则添加 securityGroup.connections.allowTo(this.rds, ec2.Port.tcp(5432));
如果需要更严格的安全控制,也可以在RDSStack中预先创建专属的应用访问安全组并暴露给PlausibleStack使用,同时让RDS安全组允许该应用安全组的访问,但上述反转规则的方式是最直接高效的解决办法。
内容的提问来源于stack exchange,提问作者Nikola-Milovic

