I’m having an issue when trying to create and replicate the secret of the database created within the provisioned cluster. I have attempted to specify the ARN of the secret in the Parameter Store within the variable /DB_SECRET/, but I have not been successful. I also tried adding a new policy to the roles, but I can’t get it to work.
My code is as follows:
rds.js
const { DatabaseClusterEngine,
ServerlessCluster,
SecurityGroup,
Port,
SubnetGroup,
Credentials,
DatabaseSecret,
AuroraCapacityUnit,
RemovalPolicy,
Duration,
} = require('aws-cdk-lib/aws-rds');
const { SubnetType } = require('aws-cdk-lib/aws-ec2');
const { SecretsManager } = require('aws-sdk');
const createRelationalDatabase = (
scope,
{ name, vpc, secretManagerKey, encryptionKey,
mySqlVersion, username, dbSubnets, subnetGroup,
cluster, securityGroup, clusterSecurityGroups, defaultDatabaseName,
dbExists, clusterIdentifier, dbSecurityGroupId, secretArn
}
) => {
if (!scope) throw new Error('Error scope missing');
if (!vpc) throw new Error('Error vpc missing');
if (!mySqlVersion) throw new Error('Error mySqlVersion missing');
if (!name) throw new Error('Error name missing');
if (!username) throw new Error('Error username missing');
if (!cluster) throw new Error('Error cluster missing');
if (!clusterSecurityGroups) throw new Error('Error clusterSecurityGroups missing');
if (!securityGroup) throw new Error('Error securityGroup missing');
let dbSecurityGroup;
let snGroup;
let dbSecret;
let relationalDb;
// Verifica version de mysql
if (!dbExists) {
const engine = DatabaseClusterEngine.auroraMysql({
version: mySqlVersion,
});
//crea grupo de seguridad
dbSecurityGroup = new SecurityGroup(
scope,
`${securityGroup.name}`,
{
vpc,
securityGroupName: `${securityGroup.name}`,
}
);
//aplica politicas anti eliminación.
dbSecurityGroup.applyRemovalPolicy(RemovalPolicy.RETAIN);
clusterSecurityGroups.forEach(clusterSecurityGroup => {
dbSecurityGroup.addIngressRule(
clusterSecurityGroup,
Port.tcp(securityGroup.port),
securityGroup.description
);
});
snGroup = new SubnetGroup(scope, `${name}-subnet-group`, {
description: subnetGroup.description,
vpc,
vpcSubnets: {
subnets: dbSubnets,
},
});
snGroup.applyRemovalPolicy(RemovalPolicy.RETAIN);
dbSecret = new DatabaseSecret(scope, `${name}-secret`, {
encryptionKey: encryptionKey,
username: username,
secretName: `${name}-rds-secret`,
});
dbSecret.applyRemovalPolicy(RemovalPolicy.RETAIN);
// Realizar la replicación del secreto
if (shouldReplicate) {
const replicatedSecret = addReplication(dbSecret);
// `replicatedSecret` ahora contiene el secreto replicado
}
replicatedSecret.applyRemovalPolicy(RemovalPolicy.RETAIN);
const credentials = Credentials.fromSecret(dbSecret, username);
// cluster con replica
//const vpc: ec2.Vpc;
relationalDb = new rds.DatabaseCluster(this, 'Database', {
engine: rds.DatabaseClusterEngine.auroraMysql({ version: rds.AuroraMysqlEngineVersion.VER_3_01_0 }),
securityGroups: [dbSecurityGroup],
storageEncryptionKey: encryptionKey,
backupRetention: Duration.days(cluster.backupRetention),
subnetGroup: snGroup,
defaultDatabaseName,
credentials,
writer: rds.ClusterInstance.provisioned('writer', {
instanceType: ec2.InstanceType.of(ec2.InstanceClass.R6G, ec2.InstanceSize.XLARGE4),
}),
serverlessV2MinCapacity: 1,
serverlessV2MaxCapacity: 2,
readers: [
// will be put in promotion tier 1 and will scale with the writer
rds.ClusterInstance.serverlessV2('reader1', { scaleWithWriter: true })
// will be put in promotion tier 2 and will not scale with the writer
//rds.ClusterInstance.serverlessV2('reader2'),
],
vpc,
});
relationalDb.applyRemovalPolicy(RemovalPolicy.RETAIN);
} else {
dbSecurityGroup = SecurityGroup.fromSecurityGroupId(scope, `${securityGroup.name}`, dbSecurityGroupId);
clusterSecurityGroups.forEach(clusterSecurityGroup => {
dbSecurityGroup.addIngressRule(
clusterSecurityGroup,
Port.tcp(securityGroup.port),
securityGroup.description
);
});
dbSecret = DatabaseSecret.fromSecretCompleteArn(scope, `${name}-secret`, secretArn);
relationalDb = ServerlessCluster.fromServerlessClusterAttributes(scope, `${name}-db`, {
clusterIdentifier,
});
}
return {
relationalDb,
dbSecret,
};
};
module.exports = { createRelationalDatabase };
my stack
cdk-iac-microservices-stack.js
const cdk = require('aws-cdk-lib');
const { getParam } = require('./iac/config');
//const { getVpcFromId } = require('@matrixtech/fw-iac/lib/network/vpc');
const { Vpc } = require('aws-cdk-lib/aws-ec2');
const { Key } = require('aws-cdk-lib/aws-kms');
const { Queue, QueueEncryption } = require('aws-cdk-lib/aws-sqs') ;
const { Stream } = require('aws-cdk-lib/aws-kinesis');
const { createSsm } = require('./iac/string-parameter');
const { createFargateCluster } = require('./iac/fargate');
const { createAdditionalPolicies } = -require('./iac/policy');
const { createDatabaseState } = require('./iac/database-state');
const { getSubnetFromId } = require('./iac/subnet');
const { createDatabase } = require('./iac/database');
const { createRelationalDatabase } = require('./iac/rds');
const { getKeyFromArn } = require('./iac/kms');
const { createBucket } = require('./iac/s3');
class CdkIacMicroservicesStack extends cdk.Stack {
/**
*
* @param {cdk.Construct} scope
* @param {string} id
* @param {cdk.StackProps=} props
*/
constructor(scope, id, props) {
if (!scope) throw new Error('scope missing');
if (!id) throw new Error('id missing');
if (!props) throw new Error('props missing');
super(scope, id, props);
let vpc = null;
let subnets = null;
if (getParam('network')) {
vpc = Vpc.fromLookup(this, 'demo-ecs-vpc', {
vpcId : getParam('network.vpcId')
});
//vpc = getVpcFromId(this, 'demo-ecs-vpc', getParam('network.vpcId') );
subnets = getParam('network.subnetsIds').map((id, index) =>
getSubnetFromId(this, `demo-sn-${index.toString()}`, id)
);
}
const stream = getParam('stream');
const sqs = getParam('sqs');
let streamKey = null;
let sqsKey = null;
let msStream = null;
let msSQS = null;
if (stream) {
streamKey = Key.fromKeyArn(
this,
`${getParam('stream.streamName')}-key-table`,
getParam('stream.kmsArn')
);
msStream = new Stream(this,`${getParam('stream.streamName')}-stream`, Object.assign({ encryptionKey: streamKey }, getParam('stream')));
}
let arraySQS=[];
// SQS
if (sqs) {
sqs.map((awssqs)=> {
sqsKey = Key.fromKeyArn(
this,
`${awssqs.sqsName}-key-sqs`,
awssqs.kmsArn
);
msSQS = new Queue(this, `${awssqs.sqsName}-sqs`,{
queueName: awssqs.sqsName,
encryption: QueueEncryption.KMS,
encryptionMasterKey: sqsKey
});
arraySQS.push(msSQS);
});
}
// BASE DE DATOS
const databaseParams = getParam('database');
let databaseKey;
if(databaseParams && databaseParams.noRelational) {
databaseKey = Key.fromKeyArn(
this,
'database-key-table',
databaseParams.noRelational.kmsArn
);
}
const roles = [];
const clusterSecurityGroups = [];
// MICROSERVICE
const microserviceContainers = [];
const microserviceProp = getParam('microservice');
if (microserviceProp) {
for (const container of microserviceProp.containers) {
const microserviceVersionTag = createSsm(
this,
Object.assign(container.ssm, {
paramExist: props.microserviceParamExist,
})
);
const isProducer = container.port ? true: false;
const microservice = createFargateCluster(
this,
Object.assign(
container,
{ vpc, subnets },
{ tag: microserviceVersionTag.stringValue },
{ isProducer, loadBalancerArn: container.loadBalancerArn },
{ securityGroupIngress: container.securityGroupIngress}
)
);
if (isProducer) {
const loadBalancer = microservice.loadBalancer;
let urlBase = `http://${loadBalancer.loadBalancerDnsName}`;
if (container.portListener) {
urlBase += `:${container.portListener}`;
}
urlBase += '/';
// URL BASE
createSsm( this, {
name: container.baseUrlParameter, valueVersionDefault: urlBase,
descriptionStringParameter: `${props.outputParameters[container.baseUrlParameter].description} - ${new Date().toISOString()}`,
paramExist: props.outputParameters[container.baseUrlParameter].exists,
ownerStack: true
});
}
roles.push(microservice.cluster.taskRole);
clusterSecurityGroups.push(microservice.securityGroup);
// additional policies permissions
const additionalPolicy = createAdditionalPolicies( this, container.policy );
microservice.cluster.taskRole.attachInlinePolicy(additionalPolicy);
microserviceContainers.push(microservice);
}
}
// Siempre se da permisos sobre el KMS para que pueda leer datos de otros streams y de dynamo
if(streamKey || sqsKey || databaseKey){
roles.forEach(role => {
if (streamKey) {
streamKey.grantEncryptDecrypt(role);
}
if (sqsKey) {
sqsKey.grantEncryptDecrypt(role);
}
if(databaseKey)
databaseKey.grantEncryptDecrypt(role);
});
}
// STREAM
if (stream) {
const { statusDatabasePolicy } = createDatabaseState( this, getParam('kinesisDatabase') );
roles.forEach(role => {
role.attachInlinePolicy(statusDatabasePolicy);
// kinesis stream permissions
msStream.grantRead(role);
msStream.grantWrite(role);
});
}
// SQS
if (sqs) {
roles.forEach(role => {
arraySQS.forEach(awssqs => {
// SQS queue permissions
awssqs.grantConsumeMessages(role);
awssqs.grantSendMessages(role);
});
});
}
// BASE DE DATOS
if (databaseParams && databaseParams.relational) {
const secretManagerKey = getKeyFromArn( this, 'secret-manager-key', databaseParams.relational.secretManagerKey );
const encriptionKey = getKeyFromArn( this, 'relational-encription-key', databaseParams.relational.encriptionKey );
const dbSubnets = getParam( 'network.sensibleSubnetsIds' ).map((id, index) =>
getSubnetFromId(this, `sensible-sn-${index.toString()}`, id)
);
const dataDb = createRelationalDatabase(
this,
Object.assign(databaseParams.relational, {
vpc,
dbSubnets,
secretManagerKey,
encriptionKey,
clusterSecurityGroups,
...props.dbData
})
);
const relationalDb = dataDb.relationalDb;
const dbSecret = dataDb.dbSecret;
roles.forEach(role=> {
dbSecret.grantRead(role);
secretManagerKey.grantEncryptDecrypt(role);
encriptionKey.grantEncryptDecrypt(role);
//relationalDb.grantDataApiAccess(role);
});
// DB_SECRET
createSsm( this, {
name: databaseParams.relational.dbSecretParameter, valueVersionDefault: dbSecret.secretName,
descriptionStringParameter: `${props.outputParameters[databaseParams.relational.dbSecretParameter].description} - ${new Date().toISOString()}`,
paramExist: props.outputParameters[databaseParams.relational.dbSecretParameter].exists,
ownerStack: true
});
}
if (databaseParams&& databaseParams.noRelational&& databaseParams.noRelational.tables) {
roles.forEach((role, index) => {
createDatabase(
this,
Object.assign(databaseParams.noRelational, {
role: role,
indexRole: index,
databaseKey,
tableExist: props.tableExist,
})
);
props.tableExist.forEach(table => {
table.exist = true;
});
});
}
// bucket and permission policy
const bucketProps = getParam('bucket');
if (bucketProps && bucketProps.createBucket) {
createBucket(
this,
Object.assign(bucketProps, {
bucketExist: props.bucketExist,
})
);
// BUCKET_NAME
createSsm( this, {
name: bucketProps.bucketNameParameter, valueVersionDefault: bucketProps.bucketName,
descriptionStringParameter: `${props.outputParameters[bucketProps.bucketNameParameter].description} - ${new Date().toISOString()}`,
paramExist: props.outputParameters[bucketProps.bucketNameParameter].exists,
ownerStack: true
});
}
// parameters
props.parameters.forEach(paramInfo => {
paramInfo.list.forEach(param => {
const parameter = createSsm(
this, {
name: `/${paramInfo.prefix}/${param.name}`,
valueVersionDefault: param.defaultValue,
descriptionStringParameter: param.description,
paramExist: param.exists
}
);
microserviceContainers.forEach(container => {
parameter.grantRead(container.cluster.taskRole);
});
});
});
// buckets
console.log(props.buckets);
props.buckets.forEach(bucketProps => {
if (bucketProps && bucketProps.createBucket) {
createBucket(
this,
bucketProps
);
// BUCKET_NAME
createSsm( this, {
name: bucketProps.bucketNameParameter, valueVersionDefault: bucketProps.bucketName,
descriptionStringParameter: `${props.outputParameters[bucketProps.bucketNameParameter].description} - ${new Date().toISOString()}`,
paramExist: props.outputParameters[bucketProps.bucketNameParameter].exists,
ownerStack: true
});
}
});
}
}
module.exports = { CdkIacMicroservicesStack }
and my file yml
stackName: IacWireTransferMicroservicesStack
accountID: '${/GLOBAL/ACCOUNT_NUMBER}'
region: us-east-1
outputParameters:
- name: /GLOBAL/PRODUCER_WIRETRANSFER_BASE_URL
description: Url del productor del dominio WIRETRANSFER
- name: /WIRETRANSFER/DB_SECRET
description: Nombre completo con sufijo del secreto que contiene la información de la conexión a la base de datos de wiretransfer
stream:
streamName: Wiretransfer
shardCount: 1
kmsArn: arn:aws:kms:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:key/${/GLOBAL/KMS_KINESIS}
sqs:
- sqsName: WireTransfer
kmsArn: arn:aws:kms:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:key/${/GLOBAL/KMS_KINESIS}
kinesisDatabase:
databaseKey: arn:aws:kms:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:key/${/GLOBAL/KMS_DYNAMO}
policyName: wiretransfer-kinesisdb
table:
name: wiretransfer-stream
tableName: wiretransfer-stream-state
bucket:
createBucket: false
network:
vpcId: ${/GLOBAL/VPC_ID}
subnetsIds:
- ${/GLOBAL/SUBNET1_ID}
- ${/GLOBAL/SUBNET2_ID}
sensibleSubnetsIds:
- ${/GLOBAL/SENSIBLE_SUBNET1_ID}
- ${/GLOBAL/SENSIBLE_SUBNET2_ID}
subnetGroupName: default
database:
isRelational: true
noRelational:
tableArn: arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/
kmsArn: arn:aws:kms:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:key/${/GLOBAL/KMS_DYNAMO}
relational:
dbSecretParameter: /WIRETRANSFER/DB_SECRET
defaultDatabaseName: wiretransfer
mySqlVersion: '8.0.34'
encriptionKey: arn:aws:kms:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:key/${/GLOBAL/KMS_RDS}
secretManagerKey: arn:aws:kms:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:key/${/GLOBAL/KMS_SECRETS}
username: wiretransferDatabaseAdmin
securityGroup:
name: vpc-group20230520172742220100000001
port: 3306
sgIngressRuleDescription: Ingress rule for consumer fargate cluster
cluster:
backupRetention: 3
subnetGroup:
description: subnet group for wiretransfer relational database
parameters:
- prefix: WIRETRANSFER
list:
- name: LOGLEVEL
description: Nivel de logs
defaultValue: 'info'
- name: URL_LOAD_SECRET
description: Default url mi post su red
defaultValue: 'https://mipossuredqa.matrixtech.com.co/main/menu?t='
- name: DB_SECRET
description: Nombre completo con sufijo del secreto que contiene la información de la conexión a la base de datos de Wiretansfer
defaultValue: wiretransfer-relational-rds-secret_23
microservice:
containers:
- baseUrlParameter: /GLOBAL/PRODUCER_WIRETRANSFER_BASE_URL
name: wiretransfer-ms
repoArn: arn:aws:ecr:us-east-1:${/GLOBAL/DEPLOYMENT_ACCOUNT_NUMBER}:repository/microservice-wiretransfer-develop
port: 8080
securityGroupIngress:
- cidr: 10.150.8.0/23
description: VPC de Developer
- cidr: 172.17.99.0/24
description: VPN Matrixtech
- cidr: 172.18.108.0/24
description: VDI Desarrollo
- cidr: 172.35.1.0/24
description: Escritorios remotos sede
- cidr: 172.35.2.0/24
description: Escritorios remotos sede
portListener: 8093
loadBalancerArn: ${/GLOBAL/GLOBAL_LOAD_BALANCER_ARN}
environment:
ENVIRONMENT: ${/GLOBAL/ENVIRONMENT}
REGION: us-east-1
PORT: '8080'
metric:
type: memory
max: 2
min: 1
percent: 80
scaleOutCooldown: 120
scaleInCooldown: 300
policy:
name: microservice-wiretransfer-policy
statements:
- 0:
action:
- s3:GetObject*
- s3:GetBucket*
- s3:List*
resource:
- arn:aws:s3:::${/GLOBAL/BUCKET_CERTS}
- arn:aws:s3:::${/GLOBAL/BUCKET_CERTS}/*
- arn:aws:s3:::configuration-${/GLOBAL/ACCOUNT_NUMBER}/*
- arn:aws:s3:::configuration-${/GLOBAL/ACCOUNT_NUMBER}
- 1:
action:
- ssm:GetParameter
resource:
- arn:aws:ssm:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:parameter/WIRETRANSFER/*
- arn:aws:ssm:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:parameter/GLOBAL/*
- arn:aws:ssm:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:parameter/PRODUCTS/KINESIS_LIMIT
- arn:aws:ssm:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:parameter/PRODUCTS/KINESIS_POLL_DELAY
- 2:
action:
- kinesis:DescribeStreamSummary
- kinesis:GetRecords
- kinesis:GetShardIterator
- kinesis:ListShards
- kinesis:PutRecord
- kinesis:PutRecords
- kinesis:SubscribeToShard
resource:
- arn:aws:kinesis:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:stream/Notifier
- arn:aws:kinesis:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:stream/Products
- arn:aws:kinesis:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:stream/Payments
- arn:aws:kinesis:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:stream/Legacy
- 3:
action:
- dynamodb:DescribeTable
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Scan
- dynamodb:UpdateItem
- dynamodb:Query
resource:
- arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/products-stream-state
- arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/master-config
- arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/mipos-tx
- arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/master-export-file
- 4:
action:
- secretsmanager:DescribeSecret
- secretsmanager:GetSecretValue
- secretsmanager:ListSecrets
- secretsmanager:ListSecretVersionIds
resource:
- arn:aws:secretsmanager:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:secret:${/GLOBAL/KEYAES}
- arn:aws:secretsmanager:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:secret:${/GLOBAL/MIPOS_SECRET}
- arn:aws:secretsmanager:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:secret:${/WIRETRANSFER/DB_SECRET}*
- 5:
action:
- dynamodb:BatchGetItem
- dynamodb:GetRecords
- dynamodb:GetShardIterator
- dynamodb:Query
- dynamodb:GetItem
- dynamodb:Scan
- dynamodb:ConditionCheckItem
- dynamodb:BatchWriteItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
resource:
- arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/transactions
- arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/hierarchy-parameters
- arn:aws:dynamodb:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:table/global-queries
- 6:
action:
- xray:*
resource:
- "*"
- 7:
action:
- sqs:SendMessage
- sqs:GetQueueAttributes
- sqs:GetQueueUrl
resource:
- arn:aws:sqs:us-east-1:${/GLOBAL/ACCOUNT_NUMBER}:Transaction
- 8:
action:
- s3:*
resource:
- arn:aws:s3:::files-${/GLOBAL/ACCOUNT_NUMBER}
- arn:aws:s3:::files-${/GLOBAL/ACCOUNT_NUMBER}/*
ssm:
name: ms-wiretransfer-version
valueVersionDefault: latest
descriptionStringParameter: name version parameter
I tried in various ways:
I specified the ARN directly in the YAML without using the parameters from GLOBAL/DB_SECRET/.
I created it manually in the Secrets Manager module, but there is an error with GetValueSecret.
I created a new policy and associated it manually with the role.
`{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:*",
"cloudformation:CreateChangeSet",
"cloudformation:DescribeChangeSet",
"cloudformation:DescribeStackResource",
"cloudformation:DescribeStacks",
"cloudformation:ExecuteChangeSet",
"docdb-elastic:GetCluster",
"docdb-elastic:ListClusters",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeVpcs",
"kms:DescribeKey",
"kms:ListAliases",
"kms:ListKeys",
"lambda:ListFunctions",
"rds:DescribeDBClusters",
"rds:DescribeDBInstances",
"redshift:DescribeClusters",
"tag:GetResources",
"secretsmanager:GetSecretValue"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"lambda:AddPermission",
"lambda:CreateFunction",
"lambda:GetFunction",
"lambda:InvokeFunction",
"lambda:UpdateFunctionConfiguration"
],
"Resource": "arn:aws:lambda:*:*:function:SecretsManager*"
},
{
"Effect": "Allow",
"Action": [
"serverlessrepo:CreateCloudFormationChangeSet",
"serverlessrepo:GetApplication"
],
"Resource": "arn:aws:serverlessrepo:*:*:applications/SecretsManager*"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::awsserverlessrepo-changesets*",
"arn:aws:s3:::secrets-manager-rotation-apps-*/*"
]
}
]
}`