From 319bea32053f21cc1dd5b88d7612eabce32ac2ba Mon Sep 17 00:00:00 2001 From: Adam Hall Date: Fri, 15 Sep 2023 14:39:00 +0930 Subject: [PATCH 1/3] MICRO-196: Slack Notifications Create a cross account supported way of sending slack notifications whenever GraphQl mesh is deployed --- packages/graphql-mesh-server/README.md | 5 +- .../graphql-mesh-server/assets/notify-sns.ts | 20 +++ .../lib/graphql-mesh-server.ts | 101 ++++++------- packages/graphql-mesh-server/lib/pipeline.ts | 133 ++++++++++++------ 4 files changed, 167 insertions(+), 92 deletions(-) create mode 100644 packages/graphql-mesh-server/assets/notify-sns.ts diff --git a/packages/graphql-mesh-server/README.md b/packages/graphql-mesh-server/README.md index 4263295b..2eca2cf7 100644 --- a/packages/graphql-mesh-server/README.md +++ b/packages/graphql-mesh-server/README.md @@ -1,6 +1,8 @@ -# Prerender in Fargate +# GraphQL Mesh in Fargate A construct host [GraphQL Mesh](https://the-guild.dev/graphql/mesh) server in Fargate. +## Deployment notifications +If notificationArn is set this construct creates a CodeStar notification rule, SNS topic and Lambda function to receive notifications for codepipeline executions and forward them to another SNS topic. This is so that you can setup AWS Chatbot either in this account OR another account and forward the notifications there. ## Props - `vpc?`: VPC to attach Redis and Fargate instances to (default: create a vpc) - `vpcName?`: If no VPC is provided create one with this name (default: 'graphql-server-vpc') @@ -13,3 +15,4 @@ A construct host [GraphQL Mesh](https://the-guild.dev/graphql/mesh) server in Fa - `memory?`: Amount of memory per Fargate instance (default: 1024) - `redis?`: Redis instance to use for mesh caching - `secrets?`: SSM values to pass through to the container as secrets + - `notificationArn?`: SNS Topic ARN to publish deployment notifications to \ No newline at end of file diff --git a/packages/graphql-mesh-server/assets/notify-sns.ts b/packages/graphql-mesh-server/assets/notify-sns.ts new file mode 100644 index 00000000..b7be432b --- /dev/null +++ b/packages/graphql-mesh-server/assets/notify-sns.ts @@ -0,0 +1,20 @@ +import { PublishCommand, SNSClient } from '@aws-sdk/client-sns'; +import { SNSEvent } from 'aws-lambda'; + +const client = new SNSClient({ region: process.env.AWS_REGION }); + +export const handler = async (event: SNSEvent): Promise => { + const record = event.Records[0]; + const message = record.Sns.Message; + + const command = new PublishCommand({ + TopicArn: process.env.SNS_TOPIC, + Message: message, + }); + + try { + await client.send(command); + } catch (e) { + console.log(e); + } +}; diff --git a/packages/graphql-mesh-server/lib/graphql-mesh-server.ts b/packages/graphql-mesh-server/lib/graphql-mesh-server.ts index 399c2457..268d2974 100644 --- a/packages/graphql-mesh-server/lib/graphql-mesh-server.ts +++ b/packages/graphql-mesh-server/lib/graphql-mesh-server.ts @@ -9,50 +9,54 @@ import { CfnCacheCluster } from "aws-cdk-lib/aws-elasticache"; import * as ssm from "aws-cdk-lib/aws-ssm"; export type MeshHostingProps = { - /** - * VPC to attach Redis and Fargate instances to (default: create a vpc) - */ - vpc?: Vpc; - /** - * If no VPC is provided create one with this name (default: 'graphql-server-vpc') - */ - vpcName?: string; - /** - * Cache node type (default: 'cache.t2.micro') - */ - cacheNodeType?: string; - /** - * Repository to pull the container image from - */ - repository?: Repository; - /** - * ARN of the certificate to add to the load balancer - */ - certificateArn: string; - /** - * Minimum number of Fargate instances - */ - minCapacity?: number; - /** - * Maximum number of Fargate instances - */ - maxCapacity?: number; - /** - * Amount of vCPU per Fargate instance (default: 512) - */ - cpu?: number; - /** - * Amount of memory per Fargate instance (default: 1024) - */ - memory?: number; - /** - * Redis instance to use for mesh caching - */ - redis?: RedisService; - /** - * SSM values to pass through to the container as secrets - */ - secrets?: { [key: string]: ssm.IStringParameter | ssm.IStringListParameter }; + /** + * VPC to attach Redis and Fargate instances to (default: create a vpc) + */ + vpc?: Vpc; + /** + * If no VPC is provided create one with this name (default: 'graphql-server-vpc') + */ + vpcName?: string; + /** + * Cache node type (default: 'cache.t2.micro') + */ + cacheNodeType?: string; + /** + * Repository to pull the container image from + */ + repository?: Repository; + /** + * ARN of the certificate to add to the load balancer + */ + certificateArn: string; + /** + * Minimum number of Fargate instances + */ + minCapacity?: number; + /** + * Maximum number of Fargate instances + */ + maxCapacity?: number; + /** + * Amount of vCPU per Fargate instance (default: 512) + */ + cpu?: number; + /** + * Amount of memory per Fargate instance (default: 1024) + */ + memory?: number; + /** + * Redis instance to use for mesh caching + */ + redis?: RedisService; + /** + * SSM values to pass through to the container as secrets + */ + secrets?: {[key: string]: ssm.IStringParameter | ssm.IStringListParameter}; + /** + * ARN of the SNS Topic to send deployment notifications to + */ + notificationArn?: string; }; export class MeshHosting extends Construct { @@ -91,9 +95,10 @@ export class MeshHosting extends Construct { this.service = mesh.service; this.repository = mesh.repository; - new CodePipelineService(this, "pipeline", { - repository: this.repository, - service: this.service, - }); + new CodePipelineService(this, 'pipeline', { + repository: this.repository, + service: this.service, + notificationArn: props.notificationArn + }); } } diff --git a/packages/graphql-mesh-server/lib/pipeline.ts b/packages/graphql-mesh-server/lib/pipeline.ts index a4e7c7c6..0b7cdd54 100644 --- a/packages/graphql-mesh-server/lib/pipeline.ts +++ b/packages/graphql-mesh-server/lib/pipeline.ts @@ -1,13 +1,19 @@ -import { Duration } from "aws-cdk-lib"; -import { Artifact, Pipeline } from "aws-cdk-lib/aws-codepipeline"; -import { Repository } from "aws-cdk-lib/aws-ecr"; -import { FargateService } from "aws-cdk-lib/aws-ecs"; -import * as pipe_actions from "aws-cdk-lib/aws-codepipeline-actions"; -import * as codebuild from "aws-cdk-lib/aws-codebuild"; -import { Construct } from "constructs"; -import * as fs from "fs"; -import * as path from "path"; -import * as YAML from "yaml"; +import { Duration } from 'aws-cdk-lib'; +import { Artifact, Pipeline } from 'aws-cdk-lib/aws-codepipeline'; +import { Repository } from 'aws-cdk-lib/aws-ecr'; +import { FargateService } from 'aws-cdk-lib/aws-ecs'; +import * as pipe_actions from 'aws-cdk-lib/aws-codepipeline-actions'; +import * as codebuild from 'aws-cdk-lib/aws-codebuild'; +import { Construct } from 'constructs'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as YAML from 'yaml'; +import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Runtime } from 'aws-cdk-lib/aws-lambda'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Topic } from 'aws-cdk-lib/aws-sns'; +import { LambdaSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'; +import { DetailType, NotificationRule } from 'aws-cdk-lib/aws-codestarnotifications'; export interface CodePipelineServiceProps { /** @@ -24,6 +30,11 @@ export interface CodePipelineServiceProps { * Path to buildspec.yml (default: '../assets/buildspec.yml') */ buildspecPath?: string; + + /** + * ARN of the SNS Topic to send deployment notifications to + */ + notificationArn?: string; } export class CodePipelineService extends Construct { @@ -58,37 +69,73 @@ export class CodePipelineService extends Construct { } ); - const buildOutput = new Artifact(); - this.pipeline.addStage({ - stageName: "Build", - actions: [ - new pipe_actions.CodeBuildAction({ - actionName: "CodeBuild", - project, - input: sourceOutput, - outputs: [buildOutput], - environmentVariables: { - IMAGE_URI: { - value: sourceAction.variables.imageUri, - }, - CONTAINER_NAME: { - value: - props.service.taskDefinition.defaultContainer?.containerName, - }, - }, - }), - ], - }); - this.pipeline.addStage({ - stageName: "Deploy", - actions: [ - new pipe_actions.EcsDeployAction({ - actionName: "DeployAction", - service: props.service, - input: buildOutput, - deploymentTimeout: Duration.minutes(10), - }), - ], - }); - } + const buildOutput = new Artifact(); + this.pipeline.addStage({ + stageName: 'Build', + actions: [ + new pipe_actions.CodeBuildAction({ + actionName: 'CodeBuild', + project, + input: sourceOutput, + outputs: [buildOutput], + environmentVariables: { + IMAGE_URI: { + value: sourceAction.variables.imageUri, + }, + CONTAINER_NAME: { + value: props.service.taskDefinition.defaultContainer + ?.containerName, + }, + }, + }), + ], + }); + this.pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new pipe_actions.EcsDeployAction({ + actionName: 'DeployAction', + service: props.service, + input: buildOutput, + deploymentTimeout: Duration.minutes(10), + }), + ], + }); + + if (props.notificationArn) { + const notifier = new NodejsFunction(this, 'NotifierLambda', { + entry: path.resolve(__dirname, '../assets/notify-sns.ts'), + description: 'Lambda function to forward SNS messages to another account.', + runtime: Runtime.NODEJS_18_X, + handler: 'index.handler', + timeout: Duration.seconds(10), + environment: { + SNS_TOPIC: props.notificationArn + } + }); + + notifier.addToRolePolicy(new PolicyStatement({ + actions: ['sns:publish'], + resources: [props.notificationArn], + effect: Effect.ALLOW + })); + + const topic = new Topic(this, 'NotifierTopic'); + topic.addSubscription(new LambdaSubscription(notifier)); + + new NotificationRule(this, 'CodeStarNotificationRule', { + detailType: DetailType.FULL, + events: [ + 'codepipeline-pipeline-pipeline-execution-failed', + 'codepipeline-pipeline-pipeline-execution-canceled', + 'codepipeline-pipeline-pipeline-execution-started', + 'codepipeline-pipeline-pipeline-execution-resumed', + 'codepipeline-pipeline-pipeline-execution-succeeded', + 'codepipeline-pipeline-pipeline-execution-superseded', + ], + targets: [topic], + source: this.pipeline, + }); + } + } } From 1e9262ea0c40e3a5be0e741e01ae5c041e58af1f Mon Sep 17 00:00:00 2001 From: Adam Hall Date: Fri, 13 Oct 2023 14:33:18 +1030 Subject: [PATCH 2/3] Add SNS client dependency --- packages/graphql-mesh-server/package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/graphql-mesh-server/package.json b/packages/graphql-mesh-server/package.json index b3a40c28..df512e91 100644 --- a/packages/graphql-mesh-server/package.json +++ b/packages/graphql-mesh-server/package.json @@ -31,6 +31,7 @@ "yaml": "^2.3.1", "aws-cdk-lib": "2.97.0", "constructs": "^10.0.0", - "source-map-support": "^0.5.21" + "source-map-support": "^0.5.21", + "@aws-sdk/client-sns": "^3.413.0" } } From 9f2aed80e1b3dc57726139d1ad045d2b6545fa67 Mon Sep 17 00:00:00 2001 From: Adam Hall Date: Mon, 18 Sep 2023 09:26:31 +0930 Subject: [PATCH 3/3] Ensure handler is included in package --- packages/graphql-mesh-server/.npmignore | 2 +- .../graphql-mesh-server/assets/{ => handlers}/notify-sns.ts | 0 packages/graphql-mesh-server/lib/pipeline.ts | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/graphql-mesh-server/assets/{ => handlers}/notify-sns.ts (100%) diff --git a/packages/graphql-mesh-server/.npmignore b/packages/graphql-mesh-server/.npmignore index bfd115ba..96724112 100644 --- a/packages/graphql-mesh-server/.npmignore +++ b/packages/graphql-mesh-server/.npmignore @@ -1,5 +1,5 @@ *.ts -!lib/handlers/*.ts +!assets/handlers/*.ts !*.d.ts !*.js diff --git a/packages/graphql-mesh-server/assets/notify-sns.ts b/packages/graphql-mesh-server/assets/handlers/notify-sns.ts similarity index 100% rename from packages/graphql-mesh-server/assets/notify-sns.ts rename to packages/graphql-mesh-server/assets/handlers/notify-sns.ts diff --git a/packages/graphql-mesh-server/lib/pipeline.ts b/packages/graphql-mesh-server/lib/pipeline.ts index 0b7cdd54..68efe7fb 100644 --- a/packages/graphql-mesh-server/lib/pipeline.ts +++ b/packages/graphql-mesh-server/lib/pipeline.ts @@ -104,7 +104,7 @@ export class CodePipelineService extends Construct { if (props.notificationArn) { const notifier = new NodejsFunction(this, 'NotifierLambda', { - entry: path.resolve(__dirname, '../assets/notify-sns.ts'), + entry: path.resolve(__dirname, '../assets/handlers/notify-sns.ts'), description: 'Lambda function to forward SNS messages to another account.', runtime: Runtime.NODEJS_18_X, handler: 'index.handler',