diff --git a/packages/@aws-cdk/aws-apigateway/README.md b/packages/@aws-cdk/aws-apigateway/README.md index ee7daa2185104..f1fba31480a77 100644 --- a/packages/@aws-cdk/aws-apigateway/README.md +++ b/packages/@aws-cdk/aws-apigateway/README.md @@ -153,6 +153,36 @@ plan.addApiStage({ }); ``` +In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`. +This construct lets you specify rate limiting properties which should be applied only to the api key being created. +The API key created has the specified rate limits, such as quota and throttles, applied. + +The following example shows how to use a rate limited api key : +```ts +const hello = new lambda.Function(this, 'hello', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'hello.handler', + code: lambda.Code.fromAsset('lambda') +}); + +const api = new apigateway.RestApi(this, 'hello-api', { }); +const integration = new apigateway.LambdaIntegration(hello); + +const v1 = api.root.addResource('v1'); +const echo = v1.addResource('echo'); +const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true }); + +const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', { + customerId: 'hello-customer', + resources: [api], + quota: { + limit: 10000, + period: apigateway.Period.MONTH + } +}); + +``` + ### Working with models When you work with Lambda integrations that are not Proxy integrations, you diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts index 1e17498cd5c2e..0a11d61c2ac78 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts @@ -20,6 +20,7 @@ export interface IApiKey extends IResourceBase { */ export interface ApiKeyProps extends ResourceOptions { /** + * [disable-awslint:ref-via-interface] * A list of resources this api key is associated with. * @default none */ diff --git a/packages/@aws-cdk/aws-apigateway/lib/index.ts b/packages/@aws-cdk/aws-apigateway/lib/index.ts index 3ce1a7901dd96..eb2b1d5dc38a8 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/index.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/index.ts @@ -7,6 +7,7 @@ export * from './stage'; export * from './integrations'; export * from './lambda-api'; export * from './api-key'; +export * from './rate-limited-api-key'; export * from './usage-plan'; export * from './vpc-link'; export * from './methodresponse'; diff --git a/packages/@aws-cdk/aws-apigateway/lib/rate-limited-api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/rate-limited-api-key.ts new file mode 100644 index 0000000000000..229f21460626a --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/lib/rate-limited-api-key.ts @@ -0,0 +1,54 @@ +import { Construct, Resource } from '@aws-cdk/core'; +import { ApiKey, ApiKeyProps, IApiKey } from './api-key'; +import { QuotaSettings, ThrottleSettings, UsagePlan, UsagePlanPerApiStage } from './usage-plan'; + +/** + * RateLimitedApiKey properties. + */ +export interface RateLimitedApiKeyProps extends ApiKeyProps { + /** + * API Stages to be associated with the RateLimitedApiKey. + * @default none + */ + readonly apiStages?: UsagePlanPerApiStage[]; + + /** + * Number of requests clients can make in a given time period. + * @default none + */ + readonly quota?: QuotaSettings; + + /** + * Overall throttle settings for the API. + * @default none + */ + readonly throttle?: ThrottleSettings; +} + +/** + * An API Gateway ApiKey, for which a rate limiting configuration can be specified. + * + * @resource AWS::ApiGateway::ApiKey + */ +export class RateLimitedApiKey extends Resource implements IApiKey { + public readonly keyId: string; + + constructor(scope: Construct, id: string, props: RateLimitedApiKeyProps = { }) { + super(scope, id, { + physicalName: props.apiKeyName, + }); + + const resource = new ApiKey(this, 'Resource', props); + + if (props.apiStages || props.quota || props.throttle) { + new UsagePlan(this, 'UsagePlanResource', { + apiKey: resource, + apiStages: props.apiStages, + quota: props.quota, + throttle: props.throttle + }); + } + + this.keyId = resource.keyId; + } +} diff --git a/packages/@aws-cdk/aws-apigateway/test/test.rate-limited-api-key.ts b/packages/@aws-cdk/aws-apigateway/test/test.rate-limited-api-key.ts new file mode 100644 index 0000000000000..3cc35eeebd29c --- /dev/null +++ b/packages/@aws-cdk/aws-apigateway/test/test.rate-limited-api-key.ts @@ -0,0 +1,108 @@ +import { expect, haveResource, ResourcePart } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { Test } from "nodeunit"; +import * as apigateway from '../lib'; + +const API_KEY_RESOURCE_TYPE = 'AWS::ApiGateway::ApiKey'; +const USAGE_PLAN_RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlan'; +const USAGE_PLAN_KEY_RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlanKey'; + +export = { + 'default setup'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'my-api', { cloudWatchRole: false, deploy: false }); + api.root.addMethod('GET'); // Need at least one method on the api + + // WHEN + new apigateway.RateLimitedApiKey(stack, 'my-api-key'); + + // THEN + // should have an api key with no props defined. + expect(stack).to(haveResource(API_KEY_RESOURCE_TYPE, undefined, ResourcePart.CompleteDefinition)); + // should not have a usage plan. + expect(stack).notTo(haveResource(USAGE_PLAN_RESOURCE_TYPE)); + // should not have a usage plan key. + expect(stack).notTo(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE)); + + test.done(); + }, + + 'only api key is created when rate limiting properties are not provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + api.root.addMethod('GET'); // api must have atleast one method. + + // WHEN + new apigateway.RateLimitedApiKey(stack, 'test-api-key', { + customerId: 'test-customer', + resources: [api] + }); + + // THEN + expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', { + CustomerId: 'test-customer', + StageKeys: [ + { + RestApiId: { Ref: "testapiD6451F70" }, + StageName: { Ref: "testapiDeploymentStagetest5869DF71" } + } + ] + })); + // should not have a usage plan. + expect(stack).notTo(haveResource(USAGE_PLAN_RESOURCE_TYPE)); + // should not have a usage plan key. + expect(stack).notTo(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE)); + + test.done(); + }, + + 'api key and usage plan are created and linked when rate limiting properties are provided'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const api = new apigateway.RestApi(stack, 'test-api', { cloudWatchRole: false, deploy: true, deployOptions: { stageName: 'test' } }); + api.root.addMethod('GET'); // api must have atleast one method. + + // WHEN + new apigateway.RateLimitedApiKey(stack, 'test-api-key', { + customerId: 'test-customer', + resources: [api], + quota: { + limit: 10000, + period: apigateway.Period.MONTH + } + }); + + // THEN + // should have an api key + expect(stack).to(haveResource('AWS::ApiGateway::ApiKey', { + CustomerId: 'test-customer', + StageKeys: [ + { + RestApiId: { Ref: "testapiD6451F70" }, + StageName: { Ref: "testapiDeploymentStagetest5869DF71" } + } + ] + })); + // should have a usage plan with specified quota. + expect(stack).to(haveResource(USAGE_PLAN_RESOURCE_TYPE, { + Quota: { + Limit: 10000, + Period: 'MONTH' + } + }, ResourcePart.Properties)); + // should have a usage plan key linking the api key and usage plan + expect(stack).to(haveResource(USAGE_PLAN_KEY_RESOURCE_TYPE, { + KeyId: { + Ref: 'testapikey998028B6' + }, + KeyType: 'API_KEY', + UsagePlanId: { + Ref: 'testapikeyUsagePlanResource66DB63D6' + } + }, ResourcePart.Properties)); + + test.done(); + } +}; \ No newline at end of file