Skip to content

Commit

Permalink
feat: plat-5642 add support for none lambda routes to authenticatedapi (
Browse files Browse the repository at this point in the history
  • Loading branch information
malcyL authored Jun 23, 2022
1 parent 96f173c commit ec68fb5
Show file tree
Hide file tree
Showing 8 changed files with 259 additions and 89 deletions.
1 change: 1 addition & 0 deletions examples/simple-authenticated-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The AuthenticatedApi:
- creates an ApiGateway (v2)
- creates an authorisation lambda which authenticates tokens against persona
- creates any number of routes, each with a lambda to handle requests.
- creates any number of routes redirecting to a given url(s).
- allows authentication to be configured either on all routes or on individual routes
- triggers an alarm if the response time on any route in the api exceeds a configurable duration
- triggers an alarm if the duration of a lambda dealing with the api requests exceeds a configurable duration
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as apigatewayv2 from "@aws-cdk/aws-apigatewayv2";
import * as cdk from "@aws-cdk/core";
import * as s3 from "@aws-cdk/aws-s3";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as sns from "@aws-cdk/aws-sns";

Expand Down Expand Up @@ -84,7 +85,7 @@ export class SimpleAuthenticatedApiStack extends cdk.Stack {
}
);

/* const api = */ new AuthenticatedApi(
const api = new AuthenticatedApi(
this,
`${prefix}simple-authenticated-api`,
{
Expand All @@ -111,23 +112,47 @@ export class SimpleAuthenticatedApiStack extends cdk.Stack {
oauth_route: "/oauth/tokens/",
},

routes: [
lambdaRoutes: [
{
name: "route1",
paths: ["/1/route1"],
path: "/1/route1",
method: apigatewayv2.HttpMethod.GET,
lambda: route1Handler,
requiredScope: "analytics:admin",
},
{
name: "route2",
paths: ["/1/route2"],
path: "/1/route2",
method: apigatewayv2.HttpMethod.GET,
lambda: route2Handler,
isPublic: true,
},
],
}
);

// It's common to want to route to static content, for example api documentation.
// This example is creating a bucket which will host documentation as a website.
// A route is then added to the api to point to the bucket.
// Note: The construct does not add the content to the bucket - you must do this yourself.
const documentationBucket = new s3.Bucket(
this,
`${prefix}simple-authenticated-api-docs`,
{
bucketName: `${prefix}simple-authenticated-api-docs`,
publicReadAccess: true,
removalPolicy: cdk.RemovalPolicy.DESTROY,
websiteIndexDocument: "index.html",
}
);

// Url Routes can be added in the initial props of the api construct, but they can also be
// added using the following method
api.addUrlRoute({
name: "simple authenticated api docs",
baseUrl: `${documentationBucket.bucketWebsiteUrl}/api-documentation/index.html`,
path: "/api-documentation",
method: apigatewayv2.HttpMethod.GET,
});
}
}
4 changes: 3 additions & 1 deletion lib/authenticated-api/authenticated-api-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@ import * as ec2 from "@aws-cdk/aws-ec2";
import * as sns from "@aws-cdk/aws-sns";

import { RouteLambdaProps } from "./route-lambda-props";
import { RouteUrlProps } from "./route-url-props";

export interface AuthenticatedApiProps {
prefix: string;
name: string;
description: string;
stageName: string;
routes: Array<RouteLambdaProps>;
lambdaRoutes?: Array<RouteLambdaProps>;
urlRoutes?: Array<RouteUrlProps>;
securityGroups?: Array<ec2.ISecurityGroup>;
vpc?: ec2.IVpc;
vpcSubnets?: ec2.SubnetSelection;
Expand Down
183 changes: 105 additions & 78 deletions lib/authenticated-api/authenticated-api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as acm from "@aws-cdk/aws-certificatemanager";
import * as apigatewayv2 from "@aws-cdk/aws-apigatewayv2";
import * as apigateway2Integrations from "@aws-cdk/aws-apigatewayv2-integrations";
import * as authorizers from "@aws-cdk/aws-apigatewayv2-authorizers";
import * as cdk from "@aws-cdk/core";
import * as cloudwatch from "@aws-cdk/aws-cloudwatch";
Expand All @@ -10,6 +11,8 @@ import * as lambdaNodeJs from "@aws-cdk/aws-lambda-nodejs";
import * as path from "path";

import { AuthenticatedApiProps } from "./authenticated-api-props";
import { RouteUrlProps } from "./route-url-props";
import { IAlarmAction } from "@aws-cdk/aws-cloudwatch";

const DEFAULT_API_LATENCY_THRESHOLD = cdk.Duration.minutes(1);
const DEFAULT_LAMBDA_DURATION_THRESHOLD = cdk.Duration.minutes(1);
Expand All @@ -18,6 +21,10 @@ export class AuthenticatedApi extends cdk.Construct {
readonly apiId: string;
readonly httpApiId: string;

private httpApi: apigatewayv2.HttpApi;
private authorizer: apigatewayv2.IHttpRouteAuthorizer;
private alarmAction: IAlarmAction;

constructor(scope: cdk.Construct, id: string, props: AuthenticatedApiProps) {
super(scope, id);

Expand Down Expand Up @@ -50,29 +57,29 @@ export class AuthenticatedApi extends cdk.Construct {
}),
};

const httpApi = new apigatewayv2.HttpApi(
this.httpApi = new apigatewayv2.HttpApi(
this,
`${props.prefix}${props.name}`,
apiGatewayProps
);

this.apiId = httpApi.apiId;
this.httpApiId = httpApi.httpApiId;
this.apiId = this.httpApi.apiId;
this.httpApiId = this.httpApi.httpApiId;

new cdk.CfnOutput(this, "apiGatewayEndpoint", {
exportName: `${props.prefix}${props.name}-endpoint`,
value: httpApi.apiEndpoint,
value: this.httpApi.apiEndpoint,
});

const alarmAction = new cloudwatchActions.SnsAction(props.alarmTopic);
this.alarmAction = new cloudwatchActions.SnsAction(props.alarmTopic);

// Routes may contain required scopes. These scopes need to be in the config
// of the authorization lambda. Create this config ahead of creating the authorization lambda
const scopeConfig: { [k: string]: string } = {};
for (const routeProps of props.routes) {
if (routeProps.requiredScope) {
for (const path of routeProps.paths) {
scopeConfig[`^${path}$`] = routeProps.requiredScope;
if (props.lambdaRoutes) {
for (const routeProps of props.lambdaRoutes) {
if (routeProps.requiredScope) {
scopeConfig[`^${routeProps.path}$`] = routeProps.requiredScope;
}
}
}
Expand Down Expand Up @@ -119,7 +126,7 @@ export class AuthenticatedApi extends cdk.Construct {
}
);

const authorizer = new authorizers.HttpLambdaAuthorizer(
this.authorizer = new authorizers.HttpLambdaAuthorizer(
"lambda-authorizer",
authLambda,
{
Expand All @@ -128,92 +135,98 @@ export class AuthenticatedApi extends cdk.Construct {
}
);

for (const routeProps of props.routes) {
const integration = new integrations.HttpLambdaIntegration(
"http-lambda-integration",
routeProps.lambda
);
if (props.urlRoutes) {
for (const routeProps of props.urlRoutes) {
this.addUrlRoute(routeProps);
}
}

if (props.lambdaRoutes) {
for (const routeProps of props.lambdaRoutes) {
const integration = new integrations.HttpLambdaIntegration(
"http-lambda-integration",
routeProps.lambda
);

for (const path of routeProps.paths) {
if (routeProps.isPublic === true) {
httpApi.addRoutes({
path: path,
this.httpApi.addRoutes({
path: routeProps.path,
methods: [routeProps.method],
integration,
});
} else {
httpApi.addRoutes({
path: path,
this.httpApi.addRoutes({
path: routeProps.path,
methods: [routeProps.method],
integration,
authorizer,
authorizer: this.authorizer,
});
}
}

// Add Cloudwatch alarms for this route
// Add Cloudwatch alarms for this route

// Add an alarm on the duration of the lambda dealing with the HTTP Request
const durationThreshold = routeProps.lamdaDurationAlarmThreshold
? routeProps.lamdaDurationAlarmThreshold
: DEFAULT_LAMBDA_DURATION_THRESHOLD;
const durationMetric = routeProps.lambda
.metric("Duration")
.with({ period: cdk.Duration.minutes(1), statistic: "sum" });
const durationAlarm = new cloudwatch.Alarm(
this,
`${props.prefix}${props.name}-${routeProps.name}-duration-alarm`,
{
alarmName: `${props.prefix}${props.name}-${routeProps.name}-duration-alarm`,
alarmDescription: `Alarm if duration of lambda for route ${
props.prefix
}${props.name}-${
routeProps.name
} exceeds duration ${durationThreshold.toMilliseconds()} milliseconds`,
actionsEnabled: true,
metric: durationMetric,
evaluationPeriods: 1,
threshold: durationThreshold.toMilliseconds(),
comparisonOperator:
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
// Set treatMissingData to IGNORE
// Stops alarms with minimal data having false alarms when they transition to this state
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
}
);
durationAlarm.addAlarmAction(alarmAction);
durationAlarm.addOkAction(alarmAction);
// Add an alarm on the duration of the lambda dealing with the HTTP Request
const durationThreshold = routeProps.lamdaDurationAlarmThreshold
? routeProps.lamdaDurationAlarmThreshold
: DEFAULT_LAMBDA_DURATION_THRESHOLD;
const durationMetric = routeProps.lambda
.metric("Duration")
.with({ period: cdk.Duration.minutes(1), statistic: "sum" });
const durationAlarm = new cloudwatch.Alarm(
this,
`${props.prefix}${props.name}-${routeProps.name}-duration-alarm`,
{
alarmName: `${props.prefix}${props.name}-${routeProps.name}-duration-alarm`,
alarmDescription: `Alarm if duration of lambda for route ${
props.prefix
}${props.name}-${
routeProps.name
} exceeds duration ${durationThreshold.toMilliseconds()} milliseconds`,
actionsEnabled: true,
metric: durationMetric,
evaluationPeriods: 1,
threshold: durationThreshold.toMilliseconds(),
comparisonOperator:
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
// Set treatMissingData to IGNORE
// Stops alarms with minimal data having false alarms when they transition to this state
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
}
);
durationAlarm.addAlarmAction(this.alarmAction);
durationAlarm.addOkAction(this.alarmAction);

const errorsMetric = routeProps.lambda
.metric("Errors")
.with({ period: cdk.Duration.minutes(1), statistic: "sum" });
const errorsMetric = routeProps.lambda
.metric("Errors")
.with({ period: cdk.Duration.minutes(1), statistic: "sum" });

const errorsAlarm = new cloudwatch.Alarm(
this,
`${props.prefix}${props.name}-${routeProps.name}-errors-alarm`,
{
alarmName: `${props.prefix}${props.name}-${routeProps.name}-errors-alarm`,
alarmDescription: `Alarm if errors on api ${props.prefix}${props.name}-${routeProps.name}`,
actionsEnabled: true,
metric: errorsMetric,
evaluationPeriods: 1,
threshold: 1,
comparisonOperator:
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
// Set treatMissingData to IGNORE
// Stops alarms with minimal data having false alarms when they transition to this state
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
}
);
errorsAlarm.addAlarmAction(alarmAction);
errorsAlarm.addOkAction(alarmAction);
const errorsAlarm = new cloudwatch.Alarm(
this,
`${props.prefix}${props.name}-${routeProps.name}-errors-alarm`,
{
alarmName: `${props.prefix}${props.name}-${routeProps.name}-errors-alarm`,
alarmDescription: `Alarm if errors on api ${props.prefix}${props.name}-${routeProps.name}`,
actionsEnabled: true,
metric: errorsMetric,
evaluationPeriods: 1,
threshold: 1,
comparisonOperator:
cloudwatch.ComparisonOperator.GREATER_THAN_OR_EQUAL_TO_THRESHOLD,
// Set treatMissingData to IGNORE
// Stops alarms with minimal data having false alarms when they transition to this state
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
}
);
errorsAlarm.addAlarmAction(this.alarmAction);
errorsAlarm.addOkAction(this.alarmAction);
}
}

// Add a cloudwatch alarm for the latency of the api - this is all routes within the api
const latencyThreshold = props.apiLatencyAlarmThreshold
? props.apiLatencyAlarmThreshold
: DEFAULT_API_LATENCY_THRESHOLD;
const metricLatency = httpApi
const metricLatency = this.httpApi
.metricLatency()
.with({ statistic: "average", period: cdk.Duration.minutes(1) });

Expand All @@ -236,7 +249,21 @@ export class AuthenticatedApi extends cdk.Construct {
treatMissingData: cloudwatch.TreatMissingData.IGNORE,
}
);
routeLatencyAlarm.addAlarmAction(alarmAction);
routeLatencyAlarm.addOkAction(alarmAction);
routeLatencyAlarm.addAlarmAction(this.alarmAction);
routeLatencyAlarm.addOkAction(this.alarmAction);
}

addUrlRoute(routeProps: RouteUrlProps) {
this.httpApi.addRoutes({
path: routeProps.path,
methods: [routeProps.method],
integration: new apigateway2Integrations.HttpUrlIntegration(
routeProps.name,
routeProps.baseUrl,
{
method: routeProps.method,
}
),
});
}
}
2 changes: 1 addition & 1 deletion lib/authenticated-api/route-lambda-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { AuthenticatedApiFunction } from "./authenticated-api-function";

export interface RouteLambdaProps {
name: string;
paths: Array<string>;
path: string;
method: apigatewayv2.HttpMethod;
isPublic?: boolean; // Defaults to false
requiredScope?: string;
Expand Down
10 changes: 10 additions & 0 deletions lib/authenticated-api/route-url-props.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as apigatewayv2 from "@aws-cdk/aws-apigatewayv2";

export interface RouteUrlProps {
name: string;
baseUrl: string;
path: string;
method: apigatewayv2.HttpMethod;
isPublic?: boolean; // Defaults to false
requiredScope?: string;
}
Loading

0 comments on commit ec68fb5

Please sign in to comment.