Skip to content

Commit

Permalink
feat: add new pattern for API Gateway with routing to multiple Lambdas (
Browse files Browse the repository at this point in the history
#1250)

* refactor!: renaming and reorganisation to make space for new API Lambda pattern

BREAKING CHANGE: A small number of types/classes have been renamed or relocated.

1. The Http5xxAlarmProps type should now be imported from "@guardian/cdk/lib/constructs/cloudwatch"
2. The Gu5xxPercentageAlarm construct is renamed: GuAlb5xxPercentageAlarm
3. The Gu5xxPercentageAlarmProps type is renamed: GuAlb5xxPercentageAlarmProps

* feat: add new pattern for API Gateway with routing to multiple Lambdas

* docs: add better documentation for new pattern and explain use-cases
  • Loading branch information
jacobwinch authored May 12, 2022
1 parent 5840385 commit a43211e
Show file tree
Hide file tree
Showing 13 changed files with 1,848 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,276 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The GuApiGateway5xxPercentageAlarm construct should create the correct alarm resource with minimal config 1`] = `
Object {
"Outputs": Object {
"RestApiEndpoint0551178A": Object {
"Value": Object {
"Fn::Join": Array [
"",
Array [
"https://",
Object {
"Ref": "RestApi0C43BF4B",
},
".execute-api.",
Object {
"Ref": "AWS::Region",
},
".",
Object {
"Ref": "AWS::URLSuffix",
},
"/",
Object {
"Ref": "RestApiDeploymentStageprod3855DE66",
},
"/",
],
],
},
},
},
"Resources": Object {
"ApiGatewayHigh5xxPercentageAlarmTesting67154503": Object {
"Properties": Object {
"ActionsEnabled": true,
"AlarmActions": Array [
Object {
"Fn::Join": Array [
"",
Array [
"arn:aws:sns:",
Object {
"Ref": "AWS::Region",
},
":",
Object {
"Ref": "AWS::AccountId",
},
":test-topic",
],
],
},
],
"AlarmDescription": "testing exceeded 1% error rate",
"AlarmName": "High 5XX error % from testing (ApiGateway) in TEST",
"ComparisonOperator": "GreaterThanThreshold",
"EvaluationPeriods": 1,
"Metrics": Array [
Object {
"Expression": "100*m1/m2",
"Id": "expr_1",
"Label": "% of 5XX responses served for testing",
},
Object {
"Id": "m1",
"MetricStat": Object {
"Metric": Object {
"Dimensions": Array [
Object {
"Name": "ApiName",
"Value": "RestApi",
},
],
"MetricName": "5XXError",
"Namespace": "AWS/ApiGateway",
},
"Period": 60,
"Stat": "Sum",
},
"ReturnData": false,
},
Object {
"Id": "m2",
"MetricStat": Object {
"Metric": Object {
"Dimensions": Array [
Object {
"Name": "ApiName",
"Value": "RestApi",
},
],
"MetricName": "Count",
"Namespace": "AWS/ApiGateway",
},
"Period": 60,
"Stat": "SampleCount",
},
"ReturnData": false,
},
],
"Threshold": 1,
"TreatMissingData": "notBreaching",
},
"Type": "AWS::CloudWatch::Alarm",
},
"RestApi0C43BF4B": Object {
"Properties": Object {
"Name": "RestApi",
"Tags": Array [
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "gu:repo",
"Value": "guardian/cdk",
},
Object {
"Key": "Stack",
"Value": "test-stack",
},
Object {
"Key": "Stage",
"Value": "TEST",
},
],
},
"Type": "AWS::ApiGateway::RestApi",
},
"RestApiAccount7C83CF5A": Object {
"DependsOn": Array [
"RestApi0C43BF4B",
],
"Properties": Object {
"CloudWatchRoleArn": Object {
"Fn::GetAtt": Array [
"RestApiCloudWatchRoleE3ED6605",
"Arn",
],
},
},
"Type": "AWS::ApiGateway::Account",
},
"RestApiCloudWatchRoleE3ED6605": Object {
"Properties": Object {
"AssumeRolePolicyDocument": Object {
"Statement": Array [
Object {
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": Object {
"Service": "apigateway.amazonaws.com",
},
},
],
"Version": "2012-10-17",
},
"ManagedPolicyArns": Array [
Object {
"Fn::Join": Array [
"",
Array [
"arn:",
Object {
"Ref": "AWS::Partition",
},
":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs",
],
],
},
],
"Tags": Array [
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "gu:repo",
"Value": "guardian/cdk",
},
Object {
"Key": "Stack",
"Value": "test-stack",
},
Object {
"Key": "Stage",
"Value": "TEST",
},
],
},
"Type": "AWS::IAM::Role",
},
"RestApiDeployment180EC50354bd4ad342d73c9ba2d68e58585d62d5": Object {
"DependsOn": Array [
"RestApiGET0F59260B",
"RestApitest9059D171",
],
"Properties": Object {
"Description": "Automatically created by the RestApi construct",
"RestApiId": Object {
"Ref": "RestApi0C43BF4B",
},
},
"Type": "AWS::ApiGateway::Deployment",
},
"RestApiDeploymentStageprod3855DE66": Object {
"DependsOn": Array [
"RestApiAccount7C83CF5A",
],
"Properties": Object {
"DeploymentId": Object {
"Ref": "RestApiDeployment180EC50354bd4ad342d73c9ba2d68e58585d62d5",
},
"RestApiId": Object {
"Ref": "RestApi0C43BF4B",
},
"StageName": "prod",
"Tags": Array [
Object {
"Key": "gu:cdk:version",
"Value": "TEST",
},
Object {
"Key": "gu:repo",
"Value": "guardian/cdk",
},
Object {
"Key": "Stack",
"Value": "test-stack",
},
Object {
"Key": "Stage",
"Value": "TEST",
},
],
},
"Type": "AWS::ApiGateway::Stage",
},
"RestApiGET0F59260B": Object {
"Properties": Object {
"AuthorizationType": "NONE",
"HttpMethod": "GET",
"Integration": Object {
"Type": "MOCK",
},
"ResourceId": Object {
"Fn::GetAtt": Array [
"RestApi0C43BF4B",
"RootResourceId",
],
},
"RestApiId": Object {
"Ref": "RestApi0C43BF4B",
},
},
"Type": "AWS::ApiGateway::Method",
},
"RestApitest9059D171": Object {
"Properties": Object {
"ParentId": Object {
"Fn::GetAtt": Array [
"RestApi0C43BF4B",
"RootResourceId",
],
},
"PathPart": "test",
"RestApiId": Object {
"Ref": "RestApi0C43BF4B",
},
},
"Type": "AWS::ApiGateway::Resource",
},
},
}
`;
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`The Gu5xxPercentageAlarm construct should create the correct alarm resource with minimal config 1`] = `
exports[`The GuAlb5xxPercentageAlarm construct should create the correct alarm resource with minimal config 1`] = `
Object {
"Outputs": Object {
"ApplicationLoadBalancerTestingDnsName": Object {
Expand Down
11 changes: 10 additions & 1 deletion src/constructs/cloudwatch/alarm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ export interface GuAlarmProps extends AlarmProps, AppIdentity {
snsTopicName: string;
}

export interface Http5xxAlarmProps
extends Omit<
GuAlarmProps,
"snsTopicName" | "evaluationPeriods" | "metric" | "period" | "threshold" | "treatMissingData" | "app"
> {
tolerated5xxPercentage: number;
numberOfMinutesAboveThresholdBeforeAlarm?: number;
}

/**
* Creates a CloudWatch alarm which sends notifications to the specified SNS topic.
*
Expand All @@ -23,7 +32,7 @@ export interface GuAlarmProps extends AlarmProps, AppIdentity {
* ```
*
* This library provides an implementation of some commonly used alarms, which require less boilerplate than this construct,
* for example [[`Gu5xxPercentageAlarm`]]. Prefer using these more specific implementations where possible.
* for example the [[`GuAlb5xxPercentageAlarm`]]. Prefer using these more specific implementations where possible.
*/
export class GuAlarm extends Alarm {
constructor(scope: GuStack, id: string, props: GuAlarmProps) {
Expand Down
67 changes: 67 additions & 0 deletions src/constructs/cloudwatch/api-gateway-alarms.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Template } from "aws-cdk-lib/assertions";
import { MockIntegration, RestApi } from "aws-cdk-lib/aws-apigateway";
import { simpleGuStackForTesting } from "../../utils/test";
import type { AppIdentity, GuStack } from "../core";
import { GuApiGateway5xxPercentageAlarm } from "./api-gateway-alarms";

const app: AppIdentity = {
app: "testing",
};

function setupBasicRestApi(stack: GuStack): RestApi {
const restApi = new RestApi(stack, "RestApi", {});
restApi.root.addResource("test");
restApi.root.addMethod("GET", new MockIntegration());
return restApi;
}

describe("The GuApiGateway5xxPercentageAlarm construct", () => {
it("should create the correct alarm resource with minimal config", () => {
const stack = simpleGuStackForTesting();
const props = {
tolerated5xxPercentage: 1,
snsTopicName: "test-topic",
};
new GuApiGateway5xxPercentageAlarm(stack, { ...app, apiGatewayInstance: setupBasicRestApi(stack), ...props });
expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
});

it("should use a custom description if one is provided", () => {
const stack = simpleGuStackForTesting();
const props = {
alarmDescription: "test-custom-alarm-description",
tolerated5xxPercentage: 1,
snsTopicName: "test-topic",
};
new GuApiGateway5xxPercentageAlarm(stack, { ...app, apiGatewayInstance: setupBasicRestApi(stack), ...props });
Template.fromStack(stack).hasResourceProperties("AWS::CloudWatch::Alarm", {
AlarmDescription: "test-custom-alarm-description",
});
});

it("should use a custom alarm name if one is provided", () => {
const stack = simpleGuStackForTesting();
const props = {
alarmName: "test-custom-alarm-name",
tolerated5xxPercentage: 1,
snsTopicName: "test-topic",
};
new GuApiGateway5xxPercentageAlarm(stack, { ...app, apiGatewayInstance: setupBasicRestApi(stack), ...props });
Template.fromStack(stack).hasResourceProperties("AWS::CloudWatch::Alarm", {
AlarmName: "test-custom-alarm-name",
});
});

it("should adjust the number of evaluation periods if a custom value is provided", () => {
const stack = simpleGuStackForTesting();
const props = {
tolerated5xxPercentage: 1,
numberOfMinutesAboveThresholdBeforeAlarm: 3,
snsTopicName: "test-topic",
};
new GuApiGateway5xxPercentageAlarm(stack, { ...app, apiGatewayInstance: setupBasicRestApi(stack), ...props });
Template.fromStack(stack).hasResourceProperties("AWS::CloudWatch::Alarm", {
EvaluationPeriods: 3,
});
});
});
Loading

0 comments on commit a43211e

Please sign in to comment.