Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(postgres-manager): failing unit test from cdk-nag #128

Merged
merged 13 commits into from
Mar 3, 2024
2 changes: 1 addition & 1 deletion config/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from '../lib/workload/orcabus-stateless-stack';
import { Duration, aws_lambda, RemovalPolicy } from 'aws-cdk-lib';
import { EventSourceProps } from '../lib/workload/stateful/event_source/component';
import { DbAuthType } from '../lib/workload/stateless/postgres_manager/function/utils';
import { DbAuthType } from '../lib/workload/stateless/postgres_manager/function/type';

const regName = 'OrcaBusSchemaRegistry';
const eventBusName = 'OrcaBusMain';
Expand Down
15 changes: 9 additions & 6 deletions lib/workload/orcabus-stateless-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Filemanager } from './stateless/filemanager/deploy/lib/filemanager';
import { Queue } from 'aws-cdk-lib/aws-sqs';
import { Secret } from 'aws-cdk-lib/aws-secretsmanager';
import {
PostgresManager,
PostgresManagerStack,
PostgresManagerConfig,
} from './stateless/postgres_manager/construct/postgresManager';
} from './stateless/postgres_manager/deploy/postgres-manager-stack';

export interface OrcaBusStatelessConfig {
multiSchemaConstructProps: MultiSchemaConstructProps;
Expand Down Expand Up @@ -41,6 +41,10 @@ export interface FilemanagerDependencies {
export class OrcaBusStatelessStack extends cdk.Stack {
private vpc: IVpc;
private lambdaSecurityGroup: ISecurityGroup;

// microservice stacks
microserviceStackArray: cdk.Stack[] = [];

constructor(scope: Construct, id: string, props: cdk.StackProps & OrcaBusStatelessConfig) {
super(scope, id, props);

Expand All @@ -63,8 +67,7 @@ export class OrcaBusStatelessStack extends cdk.Stack {

// hook microservice construct components here
this.createSequenceRunManager();

this.createPostgresManager(props.postgresManagerConfig);
this.microserviceStackArray.push(this.createPostgresManager(props.postgresManagerConfig));

if (props.filemanagerDependencies) {
this.createFilemanager({
Expand All @@ -80,7 +83,7 @@ export class OrcaBusStatelessStack extends cdk.Stack {
}

private createPostgresManager(config: PostgresManagerConfig) {
new PostgresManager(this, 'PostgresManager', {
return new PostgresManagerStack(this, 'PostgresManager', {
...config,
vpc: this.vpc,
lambdaSecurityGroup: this.lambdaSecurityGroup,
Expand Down Expand Up @@ -114,7 +117,7 @@ export class OrcaBusStatelessStack extends cdk.Stack {
dependencies.databaseSecretName
);

new Filemanager(this, 'Filemanager', {
return new Filemanager(this, 'Filemanager', {
buckets: dependencies.eventSourceBuckets,
buildEnvironment: {},
databaseSecret,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Duration } from 'aws-cdk-lib';
import { Duration, Stack, StackProps } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as nodejs from 'aws-cdk-lib/aws-lambda-nodejs';
Expand All @@ -7,7 +7,7 @@ import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as rds from 'aws-cdk-lib/aws-rds';
import * as ssm from 'aws-cdk-lib/aws-ssm';
import { DbAuthType, MicroserviceConfig } from '../function/utils';
import { MicroserviceConfig, DbAuthType } from '../function/type';

export type PostgresManagerConfig = {
masterSecretName: string;
Expand All @@ -16,13 +16,13 @@ export type PostgresManagerConfig = {
clusterResourceIdParameterName: string;
};

export type PostgresManagerStackProps = PostgresManagerConfig & {
export type PostgresManagerProps = PostgresManagerConfig & {
vpc: ec2.IVpc;
lambdaSecurityGroup: ec2.ISecurityGroup;
};

export class PostgresManager extends Construct {
constructor(scope: Construct, id: string, props: PostgresManagerStackProps) {
export class PostgresManagerStack extends Stack {
constructor(scope: Construct, id: string, props: StackProps & PostgresManagerProps) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mmalenic am thinking of making microservice to stack instead of construct. One of the thought is that we could have the control for each microservice stack (e.g. we could deploy yarn cdk-stateless-pipeline deploy ${stateless-microservice-stack} instead of the whole stateless stack). Do you have any thoughts or perhaps objection on this?

Copy link
Member

@mmalenic mmalenic Mar 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No objections, I think that's a good idea. What would be the process of deploying individual microservice stacks? Would there be a individual_stacks.ts file in bin that lists all the microservices, or is there a way deploy stacks that are nested within other stacks?

Copy link
Member Author

@williamputraintan williamputraintan Mar 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, perhaps adding in the stack construct at the bin/orcabus.ts could be it and yarn cdk-orcabus ls could list all the stacks? I temporary deploy from yarn cdk-stateless-pipeline deploy ${stateless-microservice-stack} but maybe I should try defining at bin/orcabus.ts (thou might be some namespace clashes). Not sure what is the best pattern/approach yet.

super(scope, id);

const { dbClusterIdentifier, microserviceDbConfig } = props;
Expand All @@ -33,12 +33,12 @@ export class PostgresManager extends Construct {
props.masterSecretName
);

const dbClusterResourceId = ssm.StringParameter.valueFromLookup(
const dbClusterResourceId = ssm.StringParameter.valueForStringParameter(
this,
props.clusterResourceIdParameterName
);

const rdsLambdaProps = {
const rdsLambdaProps : nodejs.NodejsFunctionProps = {
timeout: Duration.minutes(5),
depsLockFilePath: __dirname + '/../yarn.lock',
handler: 'handler',
Expand Down Expand Up @@ -98,7 +98,9 @@ export class PostgresManager extends Construct {
new iam.PolicyStatement({
actions: ['secretsmanager:CreateSecret', 'secretsmanager:TagResource'],
effect: iam.Effect.ALLOW,
resources: ['arn:aws:secretsmanager:ap-southeast-2:*:secret:*'],
resources: [
`arn:aws:secretsmanager:ap-southeast-2:${process.env.CDK_DEFAULT_ACCOUNT}:secret:*`,
],
}),
new iam.PolicyStatement({
actions: ['secretsmanager:GetRandomPassword'],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Client } from 'pg';
import {
EventType,
getMicroserviceConfig,
getMicroserviceName,
executeSqlWithLog,
getRdsMasterSecret,
} from './utils';
import { EventType } from './type';

export const handler = async (event: EventType) => {
const microserviceConfig = getMicroserviceConfig();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Client } from 'pg';
import { EventType } from './type';
import {
EventType,
getMicroserviceConfig,
getMicroserviceName,
executeSqlWithLog,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Client } from 'pg';
import {
EventType,
getMicroserviceConfig,
getMicroserviceName,
executeSqlWithLog,
getRdsMasterSecret,
DbAuthType,
} from './utils';
import { DbAuthType, EventType } from './type';

export const handler = async (event: EventType) => {
const microserviceConfig = getMicroserviceConfig();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import {
getMicroserviceName,
getMicroserviceConfig,
getRdsMasterSecret,
DbAuthType,
} from './utils';
import { DbAuthType } from './type';
import { getMicroserviceName, getMicroserviceConfig, getRdsMasterSecret } from './utils';
import {
SecretsManagerClient,
CreateSecretCommandInput,
Expand Down
16 changes: 16 additions & 0 deletions lib/workload/stateless/postgres_manager/function/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/**
* There are 2 ways of connecting from microservice to db
*/
export enum DbAuthType {
RDS_IAM,
USERNAME_PASSWORD,
}

export type EventType = {
microserviceName: string;
};

export type MicroserviceConfig = {
name: string;
authType: DbAuthType;
}[];
18 changes: 1 addition & 17 deletions lib/workload/stateless/postgres_manager/function/utils.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,6 @@
import { Client } from 'pg';
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';

/**
* There are 2 ways of connecting from microservice to db
*/
export enum DbAuthType {
RDS_IAM,
USERNAME_PASSWORD,
}

export type EventType = {
microserviceName: string;
};

export type MicroserviceConfig = {
name: string;
authType: DbAuthType;
}[];
import { MicroserviceConfig, EventType } from './type';

/**
* get microservice config from lambda environment
Expand Down
2 changes: 1 addition & 1 deletion lib/workload/stateless/postgres_manager/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "lambda-with-rds",
"name": "postgres-manager",
"packageManager": "[email protected]",
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.515.0",
Comment on lines 1 to 5
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be in the top-level package.json?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As part of the postgres-manager lambda, it will require the secret manager sdk part of the lambda asset to retrieve the master credentials and generating the random password. So I think putting at the inner package.json should be relevant?

Expand Down
88 changes: 68 additions & 20 deletions test/stateless/stateless-deployment.test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,94 @@
import { App, Aspects } from 'aws-cdk-lib';
import { App, Aspects, Stack } from 'aws-cdk-lib';
import { Annotations, Match } from 'aws-cdk-lib/assertions';
import { SynthesisMessage } from 'aws-cdk-lib/cx-api';
import { AwsSolutionsChecks } from 'cdk-nag';
import { AwsSolutionsChecks, NagSuppressions } from 'cdk-nag';
import { OrcaBusStatelessStack } from '../../lib/workload/orcabus-stateless-stack';
import { getEnvironmentConfig } from '../../config/constants';

function synthesisMessageToString(sm: SynthesisMessage): string {
return `${sm.entry.data} [${sm.id}]`;
}

// Picking prod environment to test as it contain the sensitive data
const config = getEnvironmentConfig('prod')!;

describe('cdk-nag-stateless-stack', () => {
let stack: OrcaBusStatelessStack;
let app: App;

beforeAll(() => {
app = new App({});
stack = new OrcaBusStatelessStack(app, 'TestStack', {
env: {
account: '12345678',
region: 'ap-southeast-2',
},
...config.stackProps.orcaBusStatelessConfig,
});
Aspects.of(stack).add(new AwsSolutionsChecks());

// Suppressions (if any)
// ...
const app: App = new App({});
const stack: OrcaBusStatelessStack = new OrcaBusStatelessStack(app, 'TestStack', {
env: {
account: '12345678',
region: 'ap-southeast-2',
},
...config.stackProps.orcaBusStatelessConfig,
});

test('cdk-nag AwsSolutions Pack errors', () => {
// stateless stack cdk-nag test
Aspects.of(stack).add(new AwsSolutionsChecks());
test(`OrcaBusStatelessStack: cdk-nag AwsSolutions Pack errors`, () => {
const errors = Annotations.fromStack(stack)
.findError('*', Match.stringLikeRegexp('AwsSolutions-.*'))
.map(synthesisMessageToString);
expect(errors).toHaveLength(0);
});

test('cdk-nag AwsSolutions Pack warnings', () => {
test(`OrcaBusStatelessStack: cdk-nag AwsSolutions Pack warnings`, () => {
const warnings = Annotations.fromStack(stack)
.findWarning('*', Match.stringLikeRegexp('AwsSolutions-.*'))
.map(synthesisMessageToString);
expect(warnings).toHaveLength(0);
});

// microservice cdk-nag test
for (const ms_stack of stack.microserviceStackArray) {
const stackId = ms_stack.node.id;

Aspects.of(ms_stack).add(new AwsSolutionsChecks());

applyNagSuppression(stackId, ms_stack);

test(`${stackId}: cdk-nag AwsSolutions Pack errors`, () => {
const errors = Annotations.fromStack(ms_stack)
.findError('*', Match.stringLikeRegexp('AwsSolutions-.*'))
.map(synthesisMessageToString);
expect(errors).toHaveLength(0);
});

test(`${stackId}: cdk-nag AwsSolutions Pack warnings`, () => {
const warnings = Annotations.fromStack(ms_stack)
.findWarning('*', Match.stringLikeRegexp('AwsSolutions-.*'))
.map(synthesisMessageToString);
expect(warnings).toHaveLength(0);
});
}
});

/**
* apply nag suppression according to the relevant stackId
* @param stackId the stackId
* @param stack
*/
function applyNagSuppression(stackId: string, stack: Stack) {
switch (stackId) {
case 'PostgresManager':
NagSuppressions.addStackSuppressions(stack, [
{ id: 'AwsSolutions-IAM4', reason: 'allow to use AWS managed policy' },
]);

// suppress by resource
NagSuppressions.addResourceSuppressionsByPath(
stack,
`/TestStack/PostgresManager/CreateUserPassPostgresLambda/ServiceRole/DefaultPolicy/Resource`,
[
{
id: 'AwsSolutions-IAM5',
reason:
"'*' is required for secretsmanager:GetRandomPassword and new SM ARN will contain random character",
},
]
);
break;

default:
break;
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,6 @@
"exclude": [
"node_modules",
"cdk.out",
"lib/workload/stateless/metadata_manager"
"lib/workload/stateless/**",
]
}
Loading