diff --git a/config/constants.ts b/config/constants.ts index 1771a0c6f..94822f7ba 100644 --- a/config/constants.ts +++ b/config/constants.ts @@ -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'; diff --git a/lib/workload/orcabus-stateless-stack.ts b/lib/workload/orcabus-stateless-stack.ts index 94bcbc9f5..a07f0cdec 100644 --- a/lib/workload/orcabus-stateless-stack.ts +++ b/lib/workload/orcabus-stateless-stack.ts @@ -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; @@ -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); @@ -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({ @@ -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, @@ -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, diff --git a/lib/workload/stateless/postgres_manager/construct/postgresManager.ts b/lib/workload/stateless/postgres_manager/deploy/postgres-manager-stack.ts similarity index 86% rename from lib/workload/stateless/postgres_manager/construct/postgresManager.ts rename to lib/workload/stateless/postgres_manager/deploy/postgres-manager-stack.ts index e41f07c0e..46fe88df6 100644 --- a/lib/workload/stateless/postgres_manager/construct/postgresManager.ts +++ b/lib/workload/stateless/postgres_manager/deploy/postgres-manager-stack.ts @@ -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'; @@ -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; @@ -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) { super(scope, id); const { dbClusterIdentifier, microserviceDbConfig } = props; @@ -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', @@ -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'], diff --git a/lib/workload/stateless/postgres_manager/function/alter-pg-db-owner.ts b/lib/workload/stateless/postgres_manager/function/alter-pg-db-owner.ts index 0650ae06e..f84ed6077 100644 --- a/lib/workload/stateless/postgres_manager/function/alter-pg-db-owner.ts +++ b/lib/workload/stateless/postgres_manager/function/alter-pg-db-owner.ts @@ -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(); diff --git a/lib/workload/stateless/postgres_manager/function/create-pg-db.ts b/lib/workload/stateless/postgres_manager/function/create-pg-db.ts index 32509b1e7..9e098f0db 100644 --- a/lib/workload/stateless/postgres_manager/function/create-pg-db.ts +++ b/lib/workload/stateless/postgres_manager/function/create-pg-db.ts @@ -1,6 +1,6 @@ import { Client } from 'pg'; +import { EventType } from './type'; import { - EventType, getMicroserviceConfig, getMicroserviceName, executeSqlWithLog, diff --git a/lib/workload/stateless/postgres_manager/function/create-pg-iam-role.ts b/lib/workload/stateless/postgres_manager/function/create-pg-iam-role.ts index 529cf6ef7..f4f71c2af 100644 --- a/lib/workload/stateless/postgres_manager/function/create-pg-iam-role.ts +++ b/lib/workload/stateless/postgres_manager/function/create-pg-iam-role.ts @@ -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(); diff --git a/lib/workload/stateless/postgres_manager/function/create-pg-login-role.ts b/lib/workload/stateless/postgres_manager/function/create-pg-login-role.ts index 68bd1116c..38b305679 100644 --- a/lib/workload/stateless/postgres_manager/function/create-pg-login-role.ts +++ b/lib/workload/stateless/postgres_manager/function/create-pg-login-role.ts @@ -1,9 +1,5 @@ -import { - getMicroserviceName, - getMicroserviceConfig, - getRdsMasterSecret, - DbAuthType, -} from './utils'; +import { DbAuthType } from './type'; +import { getMicroserviceName, getMicroserviceConfig, getRdsMasterSecret } from './utils'; import { SecretsManagerClient, CreateSecretCommandInput, diff --git a/lib/workload/stateless/postgres_manager/function/type.ts b/lib/workload/stateless/postgres_manager/function/type.ts new file mode 100644 index 000000000..26e743592 --- /dev/null +++ b/lib/workload/stateless/postgres_manager/function/type.ts @@ -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; +}[]; diff --git a/lib/workload/stateless/postgres_manager/function/utils.ts b/lib/workload/stateless/postgres_manager/function/utils.ts index 1110f11d8..32f12e77f 100644 --- a/lib/workload/stateless/postgres_manager/function/utils.ts +++ b/lib/workload/stateless/postgres_manager/function/utils.ts @@ -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 diff --git a/lib/workload/stateless/postgres_manager/package.json b/lib/workload/stateless/postgres_manager/package.json index 0e70c058a..ff93d24e7 100644 --- a/lib/workload/stateless/postgres_manager/package.json +++ b/lib/workload/stateless/postgres_manager/package.json @@ -1,5 +1,5 @@ { - "name": "lambda-with-rds", + "name": "postgres-manager", "packageManager": "yarn@3.5.1", "dependencies": { "@aws-sdk/client-secrets-manager": "^3.515.0", diff --git a/test/stateless/stateless-deployment.test.ts b/test/stateless/stateless-deployment.test.ts index 5f222c846..e2f4591bf 100644 --- a/test/stateless/stateless-deployment.test.ts +++ b/test/stateless/stateless-deployment.test.ts @@ -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; + } +} diff --git a/tsconfig.json b/tsconfig.json index 5f993b327..46e413c97 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,6 +30,6 @@ "exclude": [ "node_modules", "cdk.out", - "lib/workload/stateless/metadata_manager" + "lib/workload/stateless/**", ] }