Skip to content

Commit

Permalink
MICRO-196: Slack Notifications
Browse files Browse the repository at this point in the history
Create a cross account supported way of sending slack notifications whenever GraphQl mesh is deployed
  • Loading branch information
AdamJHall committed Oct 13, 2023
1 parent 3e59695 commit 319bea3
Show file tree
Hide file tree
Showing 4 changed files with 167 additions and 92 deletions.
5 changes: 4 additions & 1 deletion packages/graphql-mesh-server/README.md
Original file line number Diff line number Diff line change
@@ -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')
Expand All @@ -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
20 changes: 20 additions & 0 deletions packages/graphql-mesh-server/assets/notify-sns.ts
Original file line number Diff line number Diff line change
@@ -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<void> => {
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);
}
};
101 changes: 53 additions & 48 deletions packages/graphql-mesh-server/lib/graphql-mesh-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
});
}
}
133 changes: 90 additions & 43 deletions packages/graphql-mesh-server/lib/pipeline.ts
Original file line number Diff line number Diff line change
@@ -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 {
/**
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
});
}
}
}

0 comments on commit 319bea3

Please sign in to comment.