You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

AWS CDK中Fargate与RDS间非明显循环依赖排查与解决

AWS CDK循环依赖问题排查与解决

问题场景

逻辑上仅存在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 08:20:54