aws / aws-cdk

The AWS Cloud Development Kit is a framework for defining cloud infrastructure in code
https://aws.amazon.com/cdk
Apache License 2.0
11.57k stars 3.88k forks source link

[apigateway] ability to add methods and resources to an imported RestApi #1477

Closed tjsteinhaus closed 4 years ago

tjsteinhaus commented 5 years ago

I currently have a Stack for both my Lambda's and API Gateway methods, but I'm now hitting my resource limit. By creating a new Stack I currently don't see the ability to access/use an existing api gateway, but instead it creates a new api instead.

Is there a way to access these gateways from other stacks?

rix0rrr commented 5 years ago

You should be able to use the export/import mechanism

const restApiImportProps = restApi.export();

// ...

const api = apigw.RestApi.import(stack2, 'APIGW', restApiImportProps);
tjsteinhaus commented 5 years ago

So I just tried this approach this morning and this is the error that I receive.

UnhandledPromiseRejectionWarning: TypeError: Cannot read property 'addResource' of undefined

I was looking through the source code and I see that they do not exist in the ImportedRestApi class, only in the RestAPI class. Is this by design? If so, I'm assuming this means that it's a readonly state so I can't continue to add routes from another stack? Or is this portion incomplete?

Just seems to be an inconvenience, not really with the CDK but more with CloudFormation's limit of 200 resources.

rix0rrr commented 5 years ago

Thanks for trying, I'll classify this as a bug for now until we can get around to fixing it.

eladb commented 5 years ago

@tjsteinhaus indeed it is currently impossible to modify the model of a RestApi resource that was defined in another stack. This is why import returns an IRestApi, which doesn't have any of the addXxx methods. This is indeed a limitation of the current CDK implementation, which we should consider to revisit.

I am wondering, though, how did the typescript compiler let you use the api object returned from import in such a way that resulted in the above error? Can you share your code?

tjsteinhaus commented 5 years ago

@eladb I'm not using Typescript but just good ol' plain Javascript.

But I can show you an abbreviated version of what we're doing.

Stack 2

var api = new apigw.RestApi(this, 'myacctapi');
this.apiexport = api.export();

Stack 6

class MyStack6 extends cdk.Stack {
    constructor(parent, id, props) {
        super(parent, id, props);
        var api = apigw.RestApi.import(this, 'myacctapi', props.apiexports );
...

MyApp CDK Main Class

...
// API-gw and the backend lambda's
        var dev2 = new MyStack2(this, 'myaccount-p2', {
            ...
        });

...
var dev6 = new MyStack6(this, 'myaccount-p6', {
            apiexports: dev2.apiexport,
            stack2: dev2,
            auth: dev2.auth
        });

Stack 6 is where we are trying to extend the api gateway instead of creating a new one.

danielfariati commented 5 years ago

This feature would be really useful for me as well.

I'm basically trying to use the same API Gateway for multiple microservices. So, it would be nice if I could make a CDK stack register itself on an existing API Gateway, instead of manually configuring it every time a new microservice is added.

ggannio commented 5 years ago

Does this have a release date already?

zorrofox commented 4 years ago

+1

Poweranimal commented 4 years ago

Did anyone of you mange to split the API Gateway resources into different stacks? I'm facing the CloudFormation limit of 200 resources and I have a very hard time to separate my Api gateway into different stacks.

So far I was only able to put AWS::ApiGateway::Model and AWS::Lambda::Function and its dependencies in a separate stack. However, the major portion of my Api Gateway like AWS::ApiGateway:.Resource and so on must stay in the same stack in which the AWS::ApiGateway:.RestApi is defined.

Furthermore I faced a big problem regarding the AWS::Lambda::Function that I put in another stack: Since its references are exported and then imported by my Api Gateway stack, I'm not able to edit my Api's paths anymore. And if I use the latest released NestedStack resource (cdk version 1.12) I hit the stack input parameter limit of 60 very fast, since all of my Lambda's source code is uploaded to an S3 and cdk generates for each code resource that get uploaded to S3 three input parameters.

I really have to say that the most annoying thing of working with aws cdk are the resource limitations of AWS CloudFormation (e.g. 200 resource max in one stack and 60 parameters max in one stack). It cost so much time to work around such limitations...

I'd highly appreciate if someone could bring some light in the darkness. Maybe I'm missing something here...

AlexRex commented 4 years ago

We're having the same issue here @Poweranimal. I think the best solution would be having one api-gateway in each stack together with the lambdas. Then if you have a custom domain map it to all the separated api-gateways.

f-Stabby commented 4 years ago

@AlexRex I also have this issue,

Like above, we have a large amount of lambdas that are related to a given api gateway, I have successfully split the lambdas into multiple stacks separate to the gateway, but am unable to figure out a way to split the gateway resources into separate stacks. I am concerned about scaling my API to be larger will hit the limit of the gateway stack, Any solutions for that?

AmitBaranes commented 4 years ago

Having the same issue here, Getting the 200 resources limit so I've tried to split it into 2 stacks, no luck so far. Open a question in SOF maybe someone over there has an idea : https://stackoverflow.com/questions/59393111/cdk-split-api-gateway-stack-into-2-small-stacks

shafiqdanielAT commented 4 years ago

Edit: I've just realised that this might not correspond to the problem mentioned at the beginning. This will only help if the API Gateway resources alone does not exceed the 200 limit. If it does, then the solution by @AlexRex is the best one for now.

This is how we are doing it right now. We basically have multiple stack that share the same API Gateway class (RestApi)

class MultipleStackConstruct extends Construct {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // Main stack with shared components
    const commonStack = new CommonStack(
      scope,
      `common-stack`
    );

    // Module 1
    const moduleOneStack = new ModulOneStack(
      scope,
      `module-one`,
      {
        apiGatewayRestApi: commonStack.apiGatewayRestApi
      }
    );

    // Module 2, 3, etc.....
  }
}

This interface is used to pass the props to module stack:

export interface CommonProps extends cdk.StackProps {
  apiGatewayRestApi: apigw.RestApi;
}

The common module will create the API Gateway object:

export class CommonStack extends cdk.Stack {
  public readonly apiGatewayRestApi: apigw.RestApi;

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

    /******** SETUP API ********/
    this.apiGatewayRestApi = new apigw.RestApi(this, "MyAPI", {
      // Options here
    });
}

So the module stack itself will be something like this:

export class ModuleOneStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: CommonProps) {
    super(scope, id, props);

    if (props && props.apiGatewayRestApi) {
      const apiGatewayRestApi = props.apiGatewayRestApi;

      // associate lambda with api gateway
    }
  }
}

In this case, we are using only one API Gateway with multiple Lambdas that are divided into multiple stack, because we've also encountered the limit problem.

There is a documentation from AWS that is doing the same thing using VPC: https://docs.aws.amazon.com/cdk/api/latest/docs/aws-ec2-readme.html#sharing-vpcs-between-stacks

nassahmed commented 4 years ago

Hi, The issue seems to concern :

This makes it quite easy to reach the limit with no way of decoupling the API from the stacks where the methods / resources / models / lambda invoke permissions are declared. One possible work around is to export the RestApi ID and use Cfn constructs to build the API Gateway which defeats some of the purpose of using CDK. Another possibility is to implement your own constructs.

bomkamp commented 4 years ago

We are looking for the ability to deploy API Gateways & their resources as separate stacks. Currently, the RestApi.fromRestApiId() returns an IRestApi which does not allow use of many of the methods to create resources/methods like getRootResourceId, addMethod, addResource, getResourceByName etc.

We've tried using CfnMethod & CfnResource and are able to get it to work, but as stated in the comment above, it defeats the point of using CDK as lines as simple as:

const restApi: RestApi = RestApi.fromRestApiId(this, "RestApiGateway", 'id-here')
restApi.root.addResource('myLambda').addMethod('GET', new LambdaIntegration(myLambda));

turns into:

const apiGatewayRootResourceId: string = Fn.importValue(`${StackConfiguration.apiGatewayName}-root-resource-id`);
const apiGatewayId: string = Fn.importValue(`${StackConfiguration.apiGatewayName}-id`);

const route: CfnResource = new CfnResource(this, "ExampleResource", {
      restApiId: apiGatewayId,
      parentId: apiGatewayRootResourceId,
      pathPart: "myLambda",
});

const method: CfnMethod = new CfnMethod(this, "ExampleMethod", {
      httpMethod: HttpMethods.GET,
      resourceId: route.ref,
      restApiId: apiGatewayId,
      authorizationType: AuthorizationType.CUSTOM,
      authorizerId: authorizerId,
      integration: {
        type: IntegrationType.AWS_PROXY,
        uri: `arn:aws:apigateway:${Aws.REGION}:lambda:path/2015-03-31/functions/${myLambda.functionArn}/invocations`, 
        integrationHttpMethod: HttpMethods.POST,
      },
      operationName: "getMyLambdaResponse",
});

Forcing the use of imports/exports to pass root resource id & api gateway id. This allow disallows you to utilize defaultMethodOptions that are applied to the deployed gateway itself as you're required to provide the authorization type on the CfnMethod defined above.

maxsap commented 4 years ago

+1 on this, is there any update on it?

Townsheriff commented 4 years ago

I had same issue and since there is no answer, I'm posting my temporarily solution.

In short I'm recreating minimal context for addResource and addMethod methods, I don't know if other methods will work.

When using nested stacks pass parameters by calling exportRestApiResource and when you want to use gateway.Resource, call importRestApiResource.

Code for making: gateway.Resource work in NestedStack:

import * as gateway from "@aws-cdk/aws-apigateway";
import * as cdk from "@aws-cdk/core";
import * as assert from "assert";

interface RestApiResourceContext {
  restApi: RestApiImported;
  defaultCorsPreflightOptions: gateway.ResourceProps["defaultCorsPreflightOptions"];
  defaultMethodOptions: gateway.ResourceProps["defaultMethodOptions"];
  defaultIntegration: gateway.ResourceProps["defaultIntegration"];
  path: string;
  resourceId: string;
}

interface RestApiImportedProps {
  restApiId: string;
  latestDeployment: gateway.Deployment;
}

class RestApiImported extends cdk.Construct {
  restApiId: string;
  latestDeployment: gateway.Deployment;
  deploymentStage: gateway.Stage;

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

    this.restApiId = props.restApiId;
    this.latestDeployment = props.latestDeployment;

    const stageName = 'prod';
    this.deploymentStage = {
      restApi: this.restApiId,
      stageName: 'prod',
      urlForPath: (path: string = '/'): string => {
        if (!path.startsWith('/')) {
          throw new Error(`Path must begin with "/": ${path}`);
        }

        return `https://${this.restApiId}.execute-api.${cdk.Stack.of(this).region}.${cdk.Stack.of(this).urlSuffix}/${stageName}${path}`;
      }
    } as any;
  }

  public _attachMethod() {

  };

  public arnForExecuteApi(method: string = '*', path: string = '/*', stage: string = '*') {
    if (!path.startsWith('/')) {
      throw new Error(`"path" must begin with a "/": '${path}'`);
    }

    if (method.toUpperCase() === 'ANY') {
      method = '*';
    }

    return cdk.Stack.of(this).formatArn({
      service: 'execute-api',
      resource: this.restApiId,
      sep: '/',
      resourceName: `${stage}/${method}${path}`
    });
  }
}

export class RestApiResource extends cdk.Construct implements RestApiResourceContext {
  public defaultCorsPreflightOptions: gateway.ResourceProps["defaultCorsPreflightOptions"];
  public defaultIntegration: gateway.ResourceProps["defaultIntegration"];
  public defaultMethodOptions: gateway.ResourceProps["defaultMethodOptions"];
  public path: string;
  public resourceId: string;
  public restApi: RestApiImported;

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

    this.restApi = props.restApi;
    this.defaultCorsPreflightOptions = props.defaultCorsPreflightOptions;
    this.defaultMethodOptions = props.defaultMethodOptions;
    this.defaultIntegration = props.defaultIntegration;
    this.path = props.path;
    this.resourceId = props.resourceId;
  }

  public addResource(pathPart: string, options: gateway.ResourceOptions = {}): gateway.Resource {
    return gateway.ResourceBase.prototype.addResource.call(this, pathPart, options);
  }

  public addMethod(httpMethod: string, integration?: gateway.Integration, options: gateway.MethodOptions = {}): gateway.Method {
    return gateway.ResourceBase.prototype.addMethod.call(this, httpMethod, integration, options);
  }
}

export interface SerializedRestApiResource {
  restApiId: string;
  resourcePath: string;
  resourceId: string;
}

export function exportRestApiResource(
  resource: gateway.Resource
): SerializedRestApiResource {
  return {
    restApiId: resource.restApi.restApiId,
    resourceId: resource.resourceId,
    resourcePath: resource.path,
  };
}

export function importRestApiResource(
  stack: cdk.Stack,
  id: string,
  parameters: SerializedRestApiResource,
): RestApiResource {
  const restApiId = parameters.restApiId;
  const resourcePath = parameters.resourcePath;
  const resourceId = parameters.resourceId;

  assert(restApiId, "restApi is is falsy");
  assert(resourcePath, "resourcePath is is falsy");
  assert(resourceId, "resourceId is is falsy");

  return new RestApiResource(stack, `${id}-resource`, {
    restApi: new RestApiImported(stack, `${id}-imported`, {
      restApiId: restApiId,
      latestDeployment: new gateway.Deployment(stack, `${id}-deployment`, {
        api: gateway.RestApi.fromRestApiId(stack, `${id}-rest-api`, restApiId)
      }),
    }),
    path: resourcePath,
    resourceId: resourceId,
    defaultCorsPreflightOptions: {
      allowOrigins: gateway.Cors.ALL_ORIGINS,
      allowMethods: gateway.Cors.ALL_METHODS,
    },
    defaultMethodOptions: {},
    defaultIntegration: undefined,
  });
}
fwippe commented 4 years ago

+1

Is there any progress on this issue?

We're currently struggling with this issue as well. We've got several APIs relying on relatively large payload models, each of which is heavily documented to provide a comprehensive Swagger experience. Of course, every documentation part is counted towards the resource limit, so a limit of 200 resources is easy to exceed.

Without the means to use an imported RestApi (via RestApi.fromRestApiId()), we cannot separate models from methods, nor split the methods into multiple Stacks to circumvent resource limits.

dsdavis4 commented 4 years ago

+1

Any updates on this?

moxue1989 commented 4 years ago

+1, would really love to have this feature. Putting everything in one stack is not ideal. and no great workarounds

zoonderkins commented 4 years ago

+1

aaa-miketecson commented 4 years ago

+1 Would love the feature to add resources/methods to an existing api gateway stack.

tuanardouin commented 3 years ago

~+1 having the same limit issue~

Edit : A good explanation on the official documentation on how to handle the resource limit problem : https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigateway-readme.html#breaking-up-methods-and-resources-across-stacks

Note : apigateway.RestApi.fromRestApiAttributes returns an interface IRestApi not a class RestAPI so you can't use addModel anymore. You have to create the Model using the construct manually and passing the IRestApi to it.

nateiler commented 3 years ago

Another note is that this stack splitting currently works for adding root level resources. Ex: /books and /authors

For example it wouldn't be possible to add a 'reviews' nested stack such as /books/{id}/reviews

AmitBaranes commented 3 years ago

AWS CloudFormation now supports increased limits on five service quotas - template size, resources, parameters, mappings, and outputs. The new per template limits for the maximum number of resources is 500 (previously 200) https://aws.amazon.com/about-aws/whats-new/2020/10/aws-cloudformation-now-supports-increased-limits-on-five-service-quotas/

tvb commented 2 years ago

Despite the possibility to increase the Cloudformation resource limit, I still would like to split my Rest API Methods over multiple stacks. I believe this is still not possible?

Dan-Wuensch commented 8 months ago

Huge problem for CDK projects with more than a few API endpoints that include documentation. What's the latest on this?