如何解决AWS CDK v2中RDS与Secrets Manager栈的循环依赖错误?
问题现象
遇到循环依赖错误:
Error: 'DataPipelinePrototypeSecretStack' depends on
'DataPipelinePrototypeDatabaseStack' (DataPipelinePrototypeSecretStack
-> DataPipelinePrototypeDatabaseStack/PrototypeDb1/Resource.Ref). Adding this dependency (DataPipelinePrototypeDatabaseStack ->
DataPipelinePrototypeSecretStack/DataPipelinePrototypeMasterUserSecret/Resource.Ref)
would create a cyclic reference.
当前实现逻辑
1. 独立创建SecretStack
在单独的栈中生成数据库密钥:
export class SecretStack extends Stack { public readonly databaseMasterUserSecret: Secret; constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); this.databaseMasterUserSecret = new Secret(this, "MasterUserSecret", { secretName: "pg-master-user-secret", generateSecretString: { secretStringTemplate: JSON.stringify({ username: "postgres" }), generateStringKey: "password", passwordLength: 16, excludePunctuation: true, }, }); } }
2. DatabaseStack引用外部Secret
在数据库栈中传入外部Secret创建RDS实例:
interface DatabaseStackProps extends StackProps { vpc: Vpc, securityGroup: SecurityGroup, secret: Secret } export class DatabaseStack extends Stack { constructor(scope: Construct, id: string, props: DatabaseStackProps) { super(scope, id, props); const engine = DatabaseInstanceEngine.postgres({ version: PostgresEngineVersion.VER_15_4 }); const instanceType = InstanceType.of(InstanceClass.T3, InstanceSize.MICRO); const port = 5432; const dbName = "dbname"; const dbInstance = new DatabaseInstance(this, "TestDb1", { vpc: props.vpc, vpcSubnets: { subnetType: SubnetType.PRIVATE_ISOLATED }, instanceType, engine, port, securityGroups: [props.securityGroup], databaseName: dbName, credentials: Credentials.fromSecret(props.secret), backupRetention: Duration.days(0), deleteAutomatedBackups: true, removalPolicy: RemovalPolicy.DESTROY, }); } }
3. 入口文件的栈调用顺序
const vpcStack = new VpcStack(app, "DataPipelinePrototypeVpc"); const secretStack = new SecretStack(app, "DataPipelinePrototypeSecretStack"); new DataBaseStack(app, "DataPipelinePrototypeDatabaseStack", { vpc: vpcStack.vpc, securityGroup: vpcStack.securityGroup, secret: secretStack.databaseMasterUserSecret }); const lambdaStack = new LambdaStack(app, "DataPipelinePrototypeLambdaStack", { vpc: vpcStack.vpc, securityGroup: vpcStack.securityGroup, secret: secretStack.databaseMasterUserSecret });
调整LambdaStack与DatabaseStack的调用顺序无法解决问题;但将Secret嵌套在DatabaseStack内部时无循环依赖,不过这种方式无法给Lambda传递Secret以配置权限。
循环依赖根源
使用Credentials.fromSecret时,RDS会自动将数据库的端点、端口等连接信息写入Secret,这会触发SecretStack对DatabaseStack的反向依赖(Secret需要RDS实例的信息来更新内容),加上原本DatabaseStack依赖SecretStack的正向依赖,形成循环。
解决方案
方案1:手动指定密钥,避免RDS自动更新Secret
不使用Credentials.fromSecret,而是直接从Secret中提取密码,手动指定用户名,这样RDS不会修改Secret,消除反向依赖:
修改DatabaseStack中的Credentials配置:
const dbInstance = new DatabaseInstance(this, "TestDb1", { // ... 其他配置不变 credentials: Credentials.fromUsername("postgres", { password: props.secret.secretValueFromJson("password"), }), });
如果需要将RDS连接信息存入Secret(供Lambda使用),可以通过自定义资源手动更新:
// 引入必要的包:import { CustomResource } from 'aws-cdk-lib/custom-resources'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; // 创建自定义资源提供者Lambda const updateSecretProvider = new NodejsFunction(this, 'UpdateSecretProvider', { runtime: Runtime.NODEJS_18_X, entry: './path/to/update-secret-handler.ts', }); // 给提供者Lambda授予修改Secret的权限 props.secret.grantWrite(updateSecretProvider); // 使用自定义资源更新Secret new CustomResource(this, 'UpdateSecretWithDbInfo', { serviceToken: updateSecretProvider.functionArn, properties: { SecretArn: props.secret.secretArn, DbHost: dbInstance.dbInstanceEndpointAddress, DbPort: dbInstance.dbInstanceEndpointPort, DbName: dbName, }, });
对应的update-secret-handler.ts示例:
import { SecretsManagerClient, UpdateSecretCommand, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager"; const client = new SecretsManagerClient({}); export async function handler(event: any) { const { SecretArn, DbHost, DbPort, DbName } = event.ResourceProperties; // 获取现有Secret内容 const getSecretCmd = new GetSecretValueCommand({ SecretId: SecretArn }); const existingSecret = await client.send(getSecretCmd); const secretData = JSON.parse(existingSecret.SecretString || '{}'); // 添加RDS连接信息 secretData.host = DbHost; secretData.port = DbPort; secretData.dbname = DbName; // 更新Secret await client.send(new UpdateSecretCommand({ SecretId: SecretArn, SecretString: JSON.stringify(secretData), })); return { PhysicalResourceId: SecretArn }; }
方案2:通过栈输出导入Secret,消除隐式对象依赖
将SecretStack的Secret ARN和名称通过栈输出暴露,DatabaseStack通过导入值引用Secret,而非直接传递Secret对象,打破隐式的循环依赖链:
修改SecretStack
export class SecretStack extends Stack { public readonly secretArnOutput: CfnOutput; public readonly secretNameOutput: CfnOutput; constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const secret = new Secret(this, "MasterUserSecret", { secretName: "pg-master-user-secret", generateSecretString: { secretStringTemplate: JSON.stringify({ username: "postgres" }), generateStringKey: "password", passwordLength: 16, excludePunctuation: true, }, }); this.secretArnOutput = new CfnOutput(this, 'SecretArn', { value: secret.secretArn, exportName: 'DataPipeline-Prototype-Secret-Arn' }); this.secretNameOutput = new CfnOutput(this, 'SecretName', { value: secret.secretName, exportName: 'DataPipeline-Prototype-Secret-Name' }); } }
修改DatabaseStack的Props和实现
interface DatabaseStackProps extends StackProps { vpc: Vpc, securityGroup: SecurityGroup, } export class DatabaseStack extends Stack { constructor(scope: Construct, id: string, props: DatabaseStackProps) { super(scope, id, props); // 导入Secret const secret = Secret.fromSecretAttributes(this, 'ImportedMasterSecret', { secretArn: Fn.importValue('DataPipeline-Prototype-Secret-Arn'), secretName: Fn.importValue('DataPipeline-Prototype-Secret-Name'), }); // ... 其他配置不变 const dbInstance = new DatabaseInstance(this, "TestDb1", { // ... 其他配置 credentials: Credentials.fromSecret(secret), }); } }
修改入口文件
const vpcStack = new VpcStack(app, "DataPipelinePrototypeVpc"); const secretStack = new SecretStack(app, "DataPipelinePrototypeSecretStack"); new DataBaseStack(app, "DataPipelinePrototypeDatabaseStack", { vpc: vpcStack.vpc, securityGroup: vpcStack.securityGroup, }); const lambdaStack = new LambdaStack(app, "DataPipelinePrototypeLambdaStack", { vpc: vpcStack.vpc, securityGroup: vpcStack.securityGroup, secret: Secret.fromSecretAttributes(app, 'LambdaImportedSecret', { secretArn: Fn.importValue('DataPipeline-Prototype-Secret-Arn'), secretName: Fn.importValue('DataPipeline-Prototype-Secret-Name'), }) });
方案3:将Secret与RDS放在同一栈,通过栈输出给Lambda使用
如果业务允许,保留Secret在DatabaseStack内部,同时将Secret的ARN输出,LambdaStack通过导入值获取并配置权限:
修改DatabaseStack
export class DatabaseStack extends Stack { public readonly masterSecretArn: CfnOutput; constructor(scope: Construct, id: string, props: DatabaseStackProps) { super(scope, id, props); const engine = DatabaseInstanceEngine.postgres({ version: PostgresEngineVersion.VER_15_4 }); const instanceType = InstanceType.of(InstanceClass.T3, InstanceSize.MICRO); const port = 5432; const dbName = "dbname"; const secret = new Secret(this, "MasterUserSecret", { secretName: "pg-master-user-secret", description: "Database master user credentials", generateSecretString: { secretStringTemplate: JSON.stringify({ username: "postgres" }), generateStringKey: "password", passwordLength: 16, excludePunctuation: true, }, }); const dbInstance = new DatabaseInstance(this, "TestDb1", { // ... 其他配置 credentials: Credentials.fromSecret(secret), }); // 输出Secret ARN this.masterSecretArn = new CfnOutput(this, 'MasterSecretArn', { value: secret.secretArn, exportName: 'DataPipeline-Prototype-Master-Secret-Arn' }); } }
修改LambdaStack
interface LambdaStackProps extends StackProps { vpc: Vpc, securityGroup: SecurityGroup, } export class LambdaStack extends Stack { public readonly dataPipelineLambda: NodejsFunction; constructor(scope: Construct, id: string, props: LambdaStackProps) { super(scope, id, props); this.dataPipelineLambda = new NodejsFunction(this, "Lambda", { functionName: "dataPipelineHandler", runtime: Runtime.NODEJS_18_X, handler: "handler", entry: "./service/handler.ts" }); // 导入Secret ARN并配置权限 this.dataPipelineLambda.addToRolePolicy(new PolicyStatement({ effect: Effect.ALLOW, resources: [Fn.importValue('DataPipeline-Prototype-Master-Secret-Arn')], actions: [ "secretsmanager:GetSecretValue" ] })); } }
修改入口文件
const vpcStack = new VpcStack(app, "DataPipelinePrototypeVpc"); const dbStack = new DataBaseStack(app, "DataPipelinePrototypeDatabaseStack", { vpc: vpcStack.vpc, securityGroup: vpcStack.securityGroup, }); const lambdaStack = new LambdaStack(app, "DataPipelinePrototypeLambdaStack", { vpc: vpcStack.vpc, securityGroup: vpcStack.securityGroup, });
内容的提问来源于stack exchange,提问作者Tsar Bomba

