Skip to content

Commit

Permalink
feat(apigateway): define Resources on imported RestApi (#8270)
Browse files Browse the repository at this point in the history
The apigateway CDK construct library creates a large number of
CloudFormation resources.
To define a single Method that is backed with a Lambda function and
authorizer with an Authorizer, ~8 CloudFormation resources are created.
This quickly grows and a reasonable sized RestApi is bound to hit the
200 resource limit on their stack.

Re-organizing the CDK app into multiple `Stack`s that share the same
instance of `RestApi` will not resolve this problem, since the CDK
still makes an executive decision to keep the Methods and Resources in
the same stack as the `RestApi` construct. (see #7391)

This change introduces a new import style API `fromRestApiAttributes()`
that returns an instance of `IRestApi` that allows for new Resources to
be defined across stacks.

As a nice side effect, this change also adds the ability to define
Resources on SpecRestApi in addition to what has already been defined in
the OpenAPI spec.

closes #1477
closes #7391
fixes #8347


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
Niranjan Jayakar committed Jun 12, 2020
1 parent d21231f commit 21a1de3
Show file tree
Hide file tree
Showing 14 changed files with 906 additions and 74 deletions.
27 changes: 25 additions & 2 deletions packages/@aws-cdk/aws-apigateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ running on AWS Lambda, or any web application.
## Table of Contents

- [Defining APIs](#defining-apis)
- [Breaking up Methods and Resources across Stacks](#breaking-up-methods-and-resources-across-stacks)
- [AWS Lambda-backed APIs](#aws-lambda-backed-apis)
- [Integration Targets](#integration-targets)
- [Working with models](#working-with-models)
Expand Down Expand Up @@ -99,6 +100,18 @@ item.addMethod('GET'); // GET /items/{item}
item.addMethod('DELETE', new apigateway.HttpIntegration('http://amazon.com'));
```

### Breaking up Methods and Resources across Stacks

It is fairly common for REST APIs with a large number of Resources and Methods to hit the [CloudFormation
limit](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cloudformation-limits.html) of 200 resources per
stack.

To help with this, Resources and Methods for the same REST API can be re-organized across multiple stacks. A common
way to do this is to have a stack per Resource or groups of Resources, but this is not the only possible way.
The following example uses sets up two Resources '/pets' and '/books' in separate stacks using nested stacks:

[Resources grouped into nested stacks](test/integ.restapi-import.lit.ts)

## Integration Targets

Methods are associated with backend integrations, which are invoked when this
Expand Down Expand Up @@ -956,17 +969,27 @@ The following code creates a REST API using an external OpenAPI definition JSON
const api = new apigateway.SpecRestApi(this, 'books-api', {
apiDefinition: apigateway.ApiDefinition.fromAsset('path-to-file.json')
});

const booksResource = api.root.addResource('books')
booksResource.addMethod('GET', ...);
```
It is possible to use the `addResource()` API to define additional API Gateway Resources.
**Note:** Deployment will fail if a Resource of the same name is already defined in the Open API specification.
**Note:** Any default properties configured, such as `defaultIntegration`, `defaultMethodOptions`, etc. will only be
applied to Resources and Methods defined in the CDK, and not the ones defined in the spec. Use the [API Gateway
extensions to OpenAPI](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions.html)
to configure these.
There are a number of limitations in using OpenAPI definitions in API Gateway. Read the [Amazon API Gateway important
notes for REST APIs](https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-known-issues.html#api-gateway-known-issues-rest-apis)
for more details.
**Note:** When starting off with an OpenAPI definition using `SpecRestApi`, it is not possible to configure some
properties that can be configured directly in the OpenAPI specification file. This is to prevent people duplication
of these properties and potential confusion.
Further, it is currently also not possible to configure Methods and Resources in addition to the ones in the
specification file.
## APIGateway v2
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-apigateway/lib/authorizer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Construct, Resource, ResourceProps } from '@aws-cdk/core';
import { AuthorizationType } from './method';
import { RestApi } from './restapi';
import { IRestApi } from './restapi';

const AUTHORIZER_SYMBOL = Symbol.for('@aws-cdk/aws-apigateway.Authorizer');

Expand Down Expand Up @@ -28,7 +28,7 @@ export abstract class Authorizer extends Resource implements IAuthorizer {
* Called when the authorizer is used from a specific REST API.
* @internal
*/
public abstract _attachToApi(restApi: RestApi): void;
public abstract _attachToApi(restApi: IRestApi): void;
}

/**
Expand Down
4 changes: 2 additions & 2 deletions packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import * as lambda from '@aws-cdk/aws-lambda';
import { Construct, Duration, Lazy, Stack } from '@aws-cdk/core';
import { CfnAuthorizer } from '../apigateway.generated';
import { Authorizer, IAuthorizer } from '../authorizer';
import { RestApi } from '../restapi';
import { IRestApi } from '../restapi';

/**
* Base properties for all lambda authorizers
Expand Down Expand Up @@ -83,7 +83,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer {
* Attaches this authorizer to a specific REST API.
* @internal
*/
public _attachToApi(restApi: RestApi) {
public _attachToApi(restApi: IRestApi) {
if (this.restApiId && this.restApiId !== restApi.restApiId) {
throw new Error('Cannot attach authorizer to two different rest APIs');
}
Expand Down
27 changes: 20 additions & 7 deletions packages/@aws-cdk/aws-apigateway/lib/method.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MethodResponse } from './methodresponse';
import { IModel } from './model';
import { IRequestValidator, RequestValidatorOptions } from './requestvalidator';
import { IResource } from './resource';
import { RestApi } from './restapi';
import { IRestApi, RestApi, RestApiBase } from './restapi';
import { validateHttpMethod } from './util';

export interface MethodOptions {
Expand Down Expand Up @@ -159,13 +159,16 @@ export class Method extends Resource {

public readonly httpMethod: string;
public readonly resource: IResource;
public readonly restApi: RestApi;
/**
* The API Gateway RestApi associated with this method.
*/
public readonly api: IRestApi;

constructor(scope: Construct, id: string, props: MethodProps) {
super(scope, id);

this.resource = props.resource;
this.restApi = props.resource.restApi;
this.api = props.resource.api;
this.httpMethod = props.httpMethod.toUpperCase();

validateHttpMethod(this.httpMethod);
Expand All @@ -186,12 +189,12 @@ export class Method extends Resource {
}

if (Authorizer.isAuthorizer(authorizer)) {
authorizer._attachToApi(this.restApi);
authorizer._attachToApi(this.api);
}

const methodProps: CfnMethodProps = {
resourceId: props.resource.resourceId,
restApiId: this.restApi.restApiId,
restApiId: this.api.restApiId,
httpMethod: this.httpMethod,
operationName: options.operationName || defaultMethodOptions.operationName,
apiKeyRequired: options.apiKeyRequired || defaultMethodOptions.apiKeyRequired,
Expand All @@ -209,15 +212,25 @@ export class Method extends Resource {

this.methodId = resource.ref;

props.resource.restApi._attachMethod(this);
if (RestApiBase._isRestApiBase(props.resource.api)) {
props.resource.api._attachMethod(this);
}

const deployment = props.resource.restApi.latestDeployment;
const deployment = props.resource.api.latestDeployment;
if (deployment) {
deployment.node.addDependency(resource);
deployment.addToLogicalId({ method: methodProps });
}
}

/**
* The RestApi associated with this Method
* @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead.
*/
public get restApi(): RestApi {
return this.resource.restApi;
}

/**
* Returns an execute-api ARN for this method:
*
Expand Down
37 changes: 31 additions & 6 deletions packages/@aws-cdk/aws-apigateway/lib/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,21 @@ import { Cors, CorsOptions } from './cors';
import { Integration } from './integration';
import { MockIntegration } from './integrations';
import { Method, MethodOptions } from './method';
import { RestApi } from './restapi';
import { IRestApi, RestApi } from './restapi';

export interface IResource extends IResourceBase {
/**
* The parent of this resource or undefined for the root resource.
*/
readonly parentResource?: IResource;

/**
* The rest API that this resource is part of.
*
* @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead.
*/
readonly restApi: RestApi;

/**
* The rest API that this resource is part of.
*
Expand All @@ -20,7 +27,7 @@ export interface IResource extends IResourceBase {
* hash to determine the ID of the deployment. This allows us to automatically update
* the deployment when the model of the REST API changes.
*/
readonly restApi: RestApi;
readonly api: IRestApi;

/**
* The ID of the resource.
Expand Down Expand Up @@ -154,7 +161,11 @@ export interface ResourceProps extends ResourceOptions {

export abstract class ResourceBase extends ResourceConstruct implements IResource {
public abstract readonly parentResource?: IResource;
/**
* @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead.
*/
public abstract readonly restApi: RestApi;
public abstract readonly api: IRestApi;
public abstract readonly resourceId: string;
public abstract readonly path: string;
public abstract readonly defaultIntegration?: Integration;
Expand Down Expand Up @@ -353,14 +364,17 @@ export abstract class ResourceBase extends ResourceConstruct implements IResourc
return resource.resourceForPath(parts.join('/'));
}

/**
* @deprecated - Throws error in some use cases that have been enabled since this deprecation notice. Use `RestApi.urlForPath()` instead.
*/
public get url(): string {
return this.restApi.urlForPath(this.path);
}
}

export class Resource extends ResourceBase {
public readonly parentResource?: IResource;
public readonly restApi: RestApi;
public readonly api: IRestApi;
public readonly resourceId: string;
public readonly path: string;

Expand All @@ -380,21 +394,21 @@ export class Resource extends ResourceBase {
}

const resourceProps: CfnResourceProps = {
restApiId: props.parent.restApi.restApiId,
restApiId: props.parent.api.restApiId,
parentId: props.parent.resourceId,
pathPart: props.pathPart,
};
const resource = new CfnResource(this, 'Resource', resourceProps);

this.resourceId = resource.ref;
this.restApi = props.parent.restApi;
this.api = props.parent.api;

// render resource path (special case for root)
this.path = props.parent.path;
if (!this.path.endsWith('/')) { this.path += '/'; }
this.path += props.pathPart;

const deployment = props.parent.restApi.latestDeployment;
const deployment = props.parent.api.latestDeployment;
if (deployment) {
deployment.node.addDependency(resource);
deployment.addToLogicalId({ resource: resourceProps });
Expand All @@ -413,6 +427,17 @@ export class Resource extends ResourceBase {
this.addCorsPreflight(this.defaultCorsPreflightOptions);
}
}

/**
* The RestApi associated with this Resource
* @deprecated - Throws an error if this Resource is not associated with an instance of `RestApi`. Use `api` instead.
*/
public get restApi(): RestApi {
if (!this.parentResource) {
throw new Error('parentResource was unexpectedly not defined');
}
return this.parentResource.restApi;
}
}

export interface ProxyResourceOptions extends ResourceOptions {
Expand Down
Loading

1 comment on commit 21a1de3

@zubairzahoor
Copy link

@zubairzahoor zubairzahoor commented on 21a1de3 Jun 22, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It works but only with MockIntegration. When I try to use LambdaIntegration(lambdaFn) the following error is thrown:
RestApi is not available on Resource not connected to an instance of RestApi. Use api instead
LambdaIntegration function is using method.restApi intrinsically, hence the error. Workarounds?

Please sign in to comment.