Skip to content

Commit 036d869

Browse files
author
Niranjan Jayakar
authored
fix(apigateway): cannot remove first api key from usage plan (#13817)
The UsagePlanKey resource connects an ApiKey with a UsagePlan. The API Gateway service does not allow more than one UsagePlanKey for any given UsagePlan and ApiKey combination. For this reason, CloudFormation cannot replace this resource without either the UsagePlan or ApiKey changing. A feature was added back in Nov 2019 - 142bd0e - that allows multiple UsagePlanKey resources. The above limitation was recognized and logical id of the existing UsagePlanKey was retained. However, this unintentionally caused the logical id of the UsagePlanKey to be sensitive to order. That is, when the 'first' UsagePlanKey resource is removed, the logical id of the what was the 'second' UsagePlanKey is changed to be the logical id of what was the 'first'. This change to the logical id is, again, disallowed. To get out of this mess, we do two things - 1. introduce a feature flag that changes the default behaviour for all new CDK apps. 2. for customers with existing CDK apps who are would want to remove UsagePlanKey resource, introduce a 'overrideLogicalId' option that they can manually configure with the existing logical id. fixes #11876 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 41b3882 commit 036d869

File tree

6 files changed

+187
-82
lines changed

6 files changed

+187
-82
lines changed

packages/@aws-cdk/aws-apigateway/README.md

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ running on AWS Lambda, or any web application.
2424
- [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks)
2525
- [AWS Lambda-backed APIs](#aws-lambda-backed-apis)
2626
- [Integration Targets](#integration-targets)
27-
- [API Keys](#api-keys)
27+
- [Usage Plan & API Keys](#usage-plan--api-keys)
2828
- [Working with models](#working-with-models)
2929
- [Default Integration and Method Options](#default-integration-and-method-options)
3030
- [Proxy Routes](#proxy-routes)
@@ -168,34 +168,36 @@ const getMessageIntegration = new apigateway.AwsIntegration({
168168
});
169169
```
170170

171-
## API Keys
171+
## Usage Plan & API Keys
172172

173-
The following example shows how to use an API Key with a usage plan:
173+
A usage plan specifies who can access one or more deployed API stages and methods, and the rate at which they can be
174+
accessed. The plan uses API keys to identify API clients and meters access to the associated API stages for each key.
175+
Usage plans also allow configuring throttling limits and quota limits that are enforced on individual client API keys.
174176

175-
```ts
176-
const hello = new lambda.Function(this, 'hello', {
177-
runtime: lambda.Runtime.NODEJS_12_X,
178-
handler: 'hello.handler',
179-
code: lambda.Code.fromAsset('lambda')
180-
});
177+
The following example shows how to create and asscociate a usage plan and an API key:
181178

182-
const api = new apigateway.RestApi(this, 'hello-api', { });
183-
const integration = new apigateway.LambdaIntegration(hello);
179+
```ts
180+
const api = new apigateway.RestApi(this, 'hello-api');
184181

185182
const v1 = api.root.addResource('v1');
186183
const echo = v1.addResource('echo');
187184
const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true });
188-
const key = api.addApiKey('ApiKey');
189185

190186
const plan = api.addUsagePlan('UsagePlan', {
191187
name: 'Easy',
192-
apiKey: key,
193188
throttle: {
194189
rateLimit: 10,
195190
burstLimit: 2
196191
}
197192
});
198193

194+
const key = api.addApiKey('ApiKey');
195+
plan.addApiKey(key);
196+
```
197+
198+
To associate a plan to a given RestAPI stage:
199+
200+
```ts
199201
plan.addApiStage({
200202
stage: api.deploymentStage,
201203
throttle: [
@@ -233,26 +235,36 @@ following code provides read permission to an API key.
233235
importedKey.grantRead(lambda);
234236
```
235237

236-
In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`.
237-
This construct lets you specify rate limiting properties which should be applied only to the api key being created.
238-
The API key created has the specified rate limits, such as quota and throttles, applied.
238+
### ⚠️ Multiple API Keys
239239

240-
The following example shows how to use a rate limited api key :
240+
It is possible to specify multiple API keys for a given Usage Plan, by calling `usagePlan.addApiKey()`.
241+
242+
When using multiple API keys, a past bug of the CDK prevents API key associations to a Usage Plan to be deleted.
243+
If the CDK app had the [feature flag] - `@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId` - enabled when the API
244+
keys were created, then the app will not be affected by this bug.
245+
246+
If this is not the case, you will need to ensure that the CloudFormation [logical ids] of the API keys that are not
247+
being deleted remain unchanged.
248+
Make note of the logical ids of these API keys before removing any, and set it as part of the `addApiKey()` method:
241249

242250
```ts
243-
const hello = new lambda.Function(this, 'hello', {
244-
runtime: lambda.Runtime.NODEJS_12_X,
245-
handler: 'hello.handler',
246-
code: lambda.Code.fromAsset('lambda')
251+
usageplan.addApiKey(apiKey, {
252+
overrideLogicalId: '...',
247253
});
254+
```
248255

249-
const api = new apigateway.RestApi(this, 'hello-api', { });
250-
const integration = new apigateway.LambdaIntegration(hello);
256+
[feature flag]: https://docs.aws.amazon.com/cdk/latest/guide/featureflags.html
257+
[logical ids]: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/resources-section-structure.html
251258

252-
const v1 = api.root.addResource('v1');
253-
const echo = v1.addResource('echo');
254-
const echoMethod = echo.addMethod('GET', integration, { apiKeyRequired: true });
259+
### Rate Limited API Key
260+
261+
In scenarios where you need to create a single api key and configure rate limiting for it, you can use `RateLimitedApiKey`.
262+
This construct lets you specify rate limiting properties which should be applied only to the api key being created.
263+
The API key created has the specified rate limits, such as quota and throttles, applied.
255264

265+
The following example shows how to use a rate limited api key :
266+
267+
```ts
256268
const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', {
257269
customerId: 'hello-customer',
258270
resources: [api],
@@ -261,7 +273,6 @@ const key = new apigateway.RateLimitedApiKey(this, 'rate-limited-api-key', {
261273
period: apigateway.Period.MONTH
262274
}
263275
});
264-
265276
```
266277

267278
## Working with models

packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { Lazy, Names, Resource, Token } from '@aws-cdk/core';
1+
import { FeatureFlags, Lazy, Names, Resource, Token } from '@aws-cdk/core';
2+
import { APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID } from '@aws-cdk/cx-api';
23
import { Construct } from 'constructs';
34
import { IApiKey } from './api-key';
45
import { CfnUsagePlan, CfnUsagePlanKey } from './apigateway.generated';
@@ -139,10 +140,22 @@ export interface UsagePlanProps {
139140
/**
140141
* ApiKey to be associated with the usage plan.
141142
* @default none
143+
* @deprecated use `addApiKey()`
142144
*/
143145
readonly apiKey?: IApiKey;
144146
}
145147

148+
/**
149+
* Options to the UsagePlan.addApiKey() method
150+
*/
151+
export interface AddApiKeyOptions {
152+
/**
153+
* Override the CloudFormation logical id of the AWS::ApiGateway::UsagePlanKey resource
154+
* @default - autogenerated by the CDK
155+
*/
156+
readonly overrideLogicalId?: string;
157+
}
158+
146159
export class UsagePlan extends Resource {
147160
/**
148161
* @attribute
@@ -176,19 +189,28 @@ export class UsagePlan extends Resource {
176189
/**
177190
* Adds an ApiKey.
178191
*
179-
* @param apiKey
192+
* @param apiKey the api key to associate with this usage plan
193+
* @param options options that control the behaviour of this method
180194
*/
181-
public addApiKey(apiKey: IApiKey): void {
195+
public addApiKey(apiKey: IApiKey, options?: AddApiKeyOptions): void {
196+
let id: string;
182197
const prefix = 'UsagePlanKeyResource';
183198

184-
// Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodified.
185-
const id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix;
199+
if (FeatureFlags.of(this).isEnabled(APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID)) {
200+
id = `${prefix}:${Names.nodeUniqueId(apiKey.node)}`;
201+
} else {
202+
// Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodified.
203+
id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix;
204+
}
186205

187-
new CfnUsagePlanKey(this, id, {
206+
const resource = new CfnUsagePlanKey(this, id, {
188207
keyId: apiKey.keyId,
189208
keyType: UsagePlanKeyType.API_KEY,
190209
usagePlanId: this.usagePlanId,
191210
});
211+
if (options?.overrideLogicalId) {
212+
resource.overrideLogicalId(options?.overrideLogicalId);
213+
}
192214
}
193215

194216
/**

packages/@aws-cdk/aws-apigateway/test/integ.restapi.expected.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -602,7 +602,7 @@
602602
"UsagePlanName": "Basic"
603603
}
604604
},
605-
"myapiUsagePlanUsagePlanKeyResource050D133F": {
605+
"myapiUsagePlanUsagePlanKeyResourcetestapigatewayrestapimyapiApiKeyC43601CB600D112D": {
606606
"Type": "AWS::ApiGateway::UsagePlanKey",
607607
"Properties": {
608608
"KeyId": {

packages/@aws-cdk/aws-apigateway/test/integ.usage-plan.multikey.expected.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"myusageplan4B391740": {
44
"Type": "AWS::ApiGateway::UsagePlan"
55
},
6-
"myusageplanUsagePlanKeyResource095B4EA9": {
6+
"myusageplanUsagePlanKeyResourcetestapigatewayusageplanmultikeymyapikey1DDABC389A2809A73": {
77
"Type": "AWS::ApiGateway::UsagePlanKey",
88
"Properties": {
99
"KeyId": {

packages/@aws-cdk/aws-apigateway/test/usage-plan.test.ts

Lines changed: 101 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import '@aws-cdk/assert/jest';
22
import { ResourcePart } from '@aws-cdk/assert';
33
import * as cdk from '@aws-cdk/core';
4+
import * as cxapi from '@aws-cdk/cx-api';
5+
import { testFutureBehavior } from 'cdk-build-tools/lib/feature-flag';
46
import * as apigateway from '../lib';
57

68
const RESOURCE_TYPE = 'AWS::ApiGateway::UsagePlan';
@@ -149,60 +151,112 @@ describe('usage plan', () => {
149151
}, ResourcePart.Properties);
150152
});
151153

152-
test('UsagePlanKey', () => {
153-
// GIVEN
154-
const stack = new cdk.Stack();
155-
const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', {
156-
name: 'Basic',
154+
describe('UsagePlanKey', () => {
155+
156+
test('default', () => {
157+
// GIVEN
158+
const stack = new cdk.Stack();
159+
const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', {
160+
name: 'Basic',
161+
});
162+
const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key');
163+
164+
// WHEN
165+
usagePlan.addApiKey(apiKey);
166+
167+
// THEN
168+
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
169+
KeyId: {
170+
Ref: 'myapikey1B052F70',
171+
},
172+
KeyType: 'API_KEY',
173+
UsagePlanId: {
174+
Ref: 'myusageplan23AA1E32',
175+
},
176+
}, ResourcePart.Properties);
157177
});
158-
const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key');
159178

160-
// WHEN
161-
usagePlan.addApiKey(apiKey);
179+
test('multiple keys', () => {
180+
// GIVEN
181+
const stack = new cdk.Stack();
182+
const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan');
183+
const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', {
184+
apiKeyName: 'my-api-key-1',
185+
});
186+
const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', {
187+
apiKeyName: 'my-api-key-2',
188+
});
162189

163-
// THEN
164-
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
165-
KeyId: {
166-
Ref: 'myapikey1B052F70',
167-
},
168-
KeyType: 'API_KEY',
169-
UsagePlanId: {
170-
Ref: 'myusageplan23AA1E32',
171-
},
172-
}, ResourcePart.Properties);
173-
});
190+
// WHEN
191+
usagePlan.addApiKey(apiKey1);
192+
usagePlan.addApiKey(apiKey2);
174193

175-
test('UsagePlan can have multiple keys', () => {
176-
// GIVEN
177-
const stack = new cdk.Stack();
178-
const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan');
179-
const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', {
180-
apiKeyName: 'my-api-key-1',
194+
// THEN
195+
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
196+
Name: 'my-api-key-1',
197+
}, ResourcePart.Properties);
198+
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
199+
Name: 'my-api-key-2',
200+
}, ResourcePart.Properties);
201+
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
202+
KeyId: {
203+
Ref: 'myapikey11F723FC7',
204+
},
205+
}, ResourcePart.Properties);
206+
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
207+
KeyId: {
208+
Ref: 'myapikey2ABDEF012',
209+
},
210+
}, ResourcePart.Properties);
181211
});
182-
const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', {
183-
apiKeyName: 'my-api-key-2',
212+
213+
test('overrideLogicalId', () => {
214+
// GIVEN
215+
const app = new cdk.App();
216+
const stack = new cdk.Stack(app);
217+
const usagePlan: apigateway.UsagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan', { name: 'Basic' });
218+
const apiKey: apigateway.ApiKey = new apigateway.ApiKey(stack, 'my-api-key');
219+
220+
// WHEN
221+
usagePlan.addApiKey(apiKey, { overrideLogicalId: 'mylogicalid' });
222+
223+
// THEN
224+
const template = app.synth().getStackByName(stack.stackName).template;
225+
const logicalIds = Object.entries(template.Resources)
226+
.filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey')
227+
.map(([k, _]) => k);
228+
expect(logicalIds).toEqual(['mylogicalid']);
184229
});
185230

186-
// WHEN
187-
usagePlan.addApiKey(apiKey1);
188-
usagePlan.addApiKey(apiKey2);
231+
describe('future flag: @aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId', () => {
232+
const flags = { [cxapi.APIGATEWAY_USAGEPLANKEY_ORDERINSENSITIVE_ID]: true };
189233

190-
// THEN
191-
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
192-
Name: 'my-api-key-1',
193-
}, ResourcePart.Properties);
194-
expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', {
195-
Name: 'my-api-key-2',
196-
}, ResourcePart.Properties);
197-
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
198-
KeyId: {
199-
Ref: 'myapikey11F723FC7',
200-
},
201-
}, ResourcePart.Properties);
202-
expect(stack).toHaveResource('AWS::ApiGateway::UsagePlanKey', {
203-
KeyId: {
204-
Ref: 'myapikey2ABDEF012',
205-
},
206-
}, ResourcePart.Properties);
234+
testFutureBehavior('UsagePlanKeys have unique logical ids', flags, cdk.App, (app) => {
235+
// GIVEN
236+
const stack = new cdk.Stack(app, 'my-stack');
237+
const usagePlan = new apigateway.UsagePlan(stack, 'my-usage-plan');
238+
const apiKey1 = new apigateway.ApiKey(stack, 'my-api-key-1', {
239+
apiKeyName: 'my-api-key-1',
240+
});
241+
const apiKey2 = new apigateway.ApiKey(stack, 'my-api-key-2', {
242+
apiKeyName: 'my-api-key-2',
243+
});
244+
245+
// WHEN
246+
usagePlan.addApiKey(apiKey1);
247+
usagePlan.addApiKey(apiKey2);
248+
249+
// THEN
250+
const template = app.synth().getStackByName(stack.stackName).template;
251+
const logicalIds = Object.entries(template.Resources)
252+
.filter(([_, v]) => (v as any).Type === 'AWS::ApiGateway::UsagePlanKey')
253+
.map(([k, _]) => k);
254+
255+
expect(logicalIds).toEqual([
256+
'myusageplanUsagePlanKeyResourcemystackmyapikey1EE9AA1B359121274',
257+
'myusageplanUsagePlanKeyResourcemystackmyapikey2B4E8EB1456DC88E9',
258+
]);
259+
});
260+
});
207261
});
208262
});

0 commit comments

Comments
 (0)