I understand that sometimes order makes or breaks CloudFormation templates, but this one shouldn’t be a problem, or at least it’s not obvious to me why I’m getting this error:
Error: ‘DataPipelinePrototypeSecretStack’ depends on
‘DataPipelinePrototypeDatabaseStack’ (DataPipelinePrototypeSecretStack
-> DataPipelinePrototypeDatabaseStack/PrototypeDb1/Resource.Ref). Adding this dependency (DataPipelinePrototypeDatabaseStack ->
DataPipelinePrototypeSecretStack/DataPipelinePrototypeMasterUserSecret/Resource.Ref)
would create a cyclic reference.
I’m simply creating a secret in the SecretStack, entirely independent of RDS:
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,
},
});
}
}
Then, in the DatabaseStack, creating the RDS instance:
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), //HERE...
backupRetention: Duration.days(0),
deleteAutomatedBackups: true,
removalPolicy: RemovalPolicy.DESTROY,
});
}
}
Here’s how they’re called in the entry point:
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
});
Flipping lambdaStack and DatabaseStack order didn’t work.
When the secret was nested inside of the DatabaseStack like so, it worked just fine:
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 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", {
vpc: props.vpc,
vpcSubnets: { subnetType: SubnetType.PRIVATE_ISOLATED },
instanceType,
engine,
port,
securityGroups: [props.securityGroup],
databaseName: dbName,
credentials: Credentials.fromSecret(secret), //HERE...
backupRetention: Duration.days(0),
deleteAutomatedBackups: true,
removalPolicy: RemovalPolicy.DESTROY,
});
}
}
However, I need to add permissions to the Lambda (see entry point above), so I also need to pass the secret into the LambdaStack, so I’m able to retrieve the secret from within the Lambda code:
interface LambdaStackProps extends StackProps {
vpc: Vpc,
securityGroup: SecurityGroup,
secret: Secret
}
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"
});
this.dataPipelineLambda.addToRolePolicy(new PolicyStatement({
effect: Effect.ALLOW,
resources: [props.secret.secretArn],
actions: [
"ssm:Get*"
]
}));
}
}
2
Answers
Unless someone has a better answer, I was able to manually extract the
credentials
values in theDatabaseInstance
to make it work:Try to create the secret in the parent stack.