Aws-cdk: Parameterized swagger support for API gateway

Created on 1 Jan 2019  路  17Comments  路  Source: aws/aws-cdk

I know that Swagger integration for the API-gateway resources is on the CDK roadmap.

I think it can bring a lot of value, since at the moment, if you want to use (using CFN) a swagger file you can either inline it and reference CFN resources, or import it and not reference anything.

Typically, you'd want both - you want to separate the swagger file from your CFN/CDK code so that you can use all the fancy tools (UI editor / client generation / etc), but also usually you'd need to reference CFN resources (e.g. lambda integrations).

With CDK it can be possible to have a templated external swagger file, and use string replacements for the referenced resources.

Took this offline with @eladb who suggested something in the lines of:

new apigateway.RestApi(this, 'MyAPI', {
  swagger: Swagger.load('/path/to/swagger', {
    xxx: myLambdaFunction.functionName,
    yyy: myAuthenticator.functionArn
  }
});

I think it could bring huge value to CDK users as you can use the "API first" methodology and leverage all the existing swagger tools.

@aws-cdaws-apigateway efforlarge feature-request p1

Most helpful comment

At the moment I'm working around this by using the swagger-parser npm library to parse my swagger file on start up, and then dynamically add lambda integrations. Disclaimer I don't know how well cdk will continue to handle running promises but as of 0.34.0 it works.

Hope this helps people workaround this until it's implemented:
https://gist.github.com/abbottdev/17379763ebc14a5ecbf2a111ffbcdd86

It may be a bit dynamic for people to want to use, it could always be refactored to use more explicitly defined lambda functions quite easily.

All 17 comments

Before we implement built in support for swagger parameterization, users can consider something like https://ejs.co

At the moment I'm working around this by using the swagger-parser npm library to parse my swagger file on start up, and then dynamically add lambda integrations. Disclaimer I don't know how well cdk will continue to handle running promises but as of 0.34.0 it works.

Hope this helps people workaround this until it's implemented:
https://gist.github.com/abbottdev/17379763ebc14a5ecbf2a111ffbcdd86

It may be a bit dynamic for people to want to use, it could always be refactored to use more explicitly defined lambda functions quite easily.

Before we implement built in support for swagger parameterization, users can consider something like https://ejs.co

@eladb How would you resolve a Lambda ARN (for x-amazon-apigateway-integration) at the time you process the EJS template?

@ryan-mars If you plug in stringified tokens into the template, the CDK will resolve them during synthesis and render the swagger definition as a CFN expression that resolves these values in deployment.

@ryan-mars If you plug in stringified tokens into the template, the CDK will resolve them during synthesis and render the swagger definition as a CFN expression that resolves these values in deployment.

Tried ejs.renderfile and it's resolving to the arn to ${Token[TOKEN.77]} on the CFN template and if Fn:Sub is still used, it can cause an error. So, in the URI, I replaced ${AWS::Region} in order to get rid of Fn:Sub. And it solves the problem. I am wondering if there is a better way

@ywauto83 Using the yaml npm module I read in the OAS file. I manilpulate the values in the OAS file as needed using CDK constructs (which use tokens obviously) and pass the javascript object to the body parameter of CfnApi (which expects a JSON object according to the CloudFormation documentation). Apparently the CDK construct takes a JS object in that case. It works fine and I didn't need to use ejs after all.

I manilpulate the values in the OAS file as needed using CDK constructs (which use tokens obviously)
@ryan-mars
This is the key part I was trying to understand better. In my way, I used ejs to replace<%=tag%> in the OAS file. How did you manipulate the values only with CDK constructs? Cheers!

I solved. Basically how @ryan-mars describes it but I think my solution is a bit more nice.

const api = new apigateway.RestApi(this, 'itemsApi', {
  restApiName: 'Items Service'
});

const cfnApi = api.node.defaultChild as apigateway.CfnRestApi;
// Upload Swagger to S3
const fileAsset = new assets.Asset(this, 'SwaggerAsset', {
  path: join(__dirname, 'templates/swagger.yaml')
});

cfnApi.bodyS3Location = {bucket: fileAsset.bucket.bucketName, key: fileAsset.s3ObjectKey };

Export from existing API Gatway the swagger yaml with API Gateway extension
In the code it first uploads the swagger.yaml file to s3. Than it sets the bodyS3Location property to RestApi in CFN.

Don't forget to set the validatior!

const validator = api.addRequestValidator('DefaultValidator', {
  validateRequestBody: true,
  validateRequestParameters: true
});

const createOneIntegration = new apigateway.LambdaIntegration(createOneApi;

items.addMethod('POST', createOneIntegration, { requestValidator: validator});

Oh boy that was something :)

This is the key part I was trying to understand better. In my way, I used ejs to replace<%=tag%> in the OAS file. How did you manipulate the values only with CDK constructs? Cheers!

@ywauto83 Apologies for not seeing your reply. Here's what I'm doing...

this is ugly buuuuuuuut

const file = readFileSync("customer.v1.openapi.yaml", "utf8");

let spec = YAML.parse(file);

const api = new HttpApi(this, "CustomerAPI", {
  name: "API for customers",
  openApiSpec: spec,
  integrations: [
    { path: "/users", method: "post" },
    { path: "/users", method: "get" },
    { path: "/users/{id}", method: "get" }
  ]
});
interface HttpApiIntegrationProps {
  path: string;
  method: string;
}

interface HttpApiProps {
  name: string;
  openApiSpec: any;
  integrations: Array<HttpApiIntegrationProps>;
}

export class HttpApi extends cdk.Construct {
  readonly cfnApi: apigatewayv2.CfnApi;
  readonly logGroup: LogGroup;
  readonly stage: apigatewayv2.CfnStage;
  readonly functions: Array<lambda.Function>;

  constructor(scope: cdk.Construct, id: string, props: HttpApiProps) {
    super(scope, id);
    this.functions = [];

    const stack = Stack.of(this);
    let spec = props.openApiSpec;

    props.integrations.map(e => {
      if (spec?.paths?.[e.path]?.[e.method] == undefined) {
        const error = `There is no path in the Open API Spec matching ${e.method} ${e.path}`;
        console.error(error);
        throw error;
      } else {
        const cleanPath = e.path.replace(/[{}]/gi, "");
        const func = new NodejsFunction(this, `${e.method}${cleanPath}`, {
          entry: join("src", "api", ...cleanPath.split("/"), `${e.method}.ts`)
        });
        this.functions.push(func);
        spec.paths[e.path][e.method]["x-amazon-apigateway-integration"] = {
          type: "AWS_PROXY",
          httpMethod: "POST",
          uri: func.functionArn,
          payloadFormatVersion: "1.0"
        };
      }
    });

    this.cfnApi = new apigatewayv2.CfnApi(this, "HttpApi", {
      body: spec
    });

    this.functions.map((f, i) => {
      new lambda.CfnPermission(this, `LambdaPermission_${i}`, {
        action: "lambda:InvokeFunction",
        principal: "apigateway.amazonaws.com",
        functionName: f.functionName,
        sourceArn: `arn:${stack.partition}:execute-api:${stack.region}:${stack.account}:${this.cfnApi.ref}/*/*`
      });
    });

    this.logGroup = new LogGroup(this, "DefaultStageAccessLogs");

    this.stage = new apigatewayv2.CfnStage(this, "DefaultStage", {
      apiId: this.cfnApi.ref,
      stageName: "$default",
      autoDeploy: true,
      accessLogSettings: {
        destinationArn: this.logGroup.logGroupArn,
        format:
          '{ "requestId":"$context.requestId", "ip": "$context.identity.sourceIp", "requestTime":"$context.requestTime", "httpMethod":"$context.httpMethod","routeKey":"$context.routeKey", "status":"$context.status","protocol":"$context.protocol", "responseLength":"$context.responseLength" }'
      }
    });
  }
}

Then I have one file (exporting a single function 'handler') for each path/method. Like

src/api/users/get.ts
src/api/users/post.ts
src/api/users/id/get.ts
etc..

Thanks for sharing!

one update more. There is a little caveat. If you kind of change an api lambda the uri of it changes as well and is not correct anymore in the api gateway if you used swagger. Its basically the problem the creator of this issue describes.

I solved it with cdk deploy two times

export WITH_SWAGGER='false' && cdk deploy
merge extracted Swagger and Swagger validation file ...
export WITH_SWAGGER='true' && cdk deploy

The first one without using the swagger file. Than I cli extract the swagger file. This one now I merge with a swagger which only contains the parameter validations kind of:
swagger_validation.yaml MERGE swagger_new.yaml

that generates swagger_full.yaml .

swagger_full.yaml is now clean with updated Lambda uris and will be used for the second deploy.

I will write a blog post about that workaround and post it here.

Martin's blog post FYI: https://martinmueller.dev/cdk-swagger-eng/#:~:text=In%20the%20last%20post%20I,for%20describing%20your%20cloud%20infrastructure.&text=When%20using%20AWS%20API%20Gateway,Query%2C%20Path%20and%20Body%20Parameter.

As this was something I was just searching for and came up. Haven't tried it yet but thanks to Martin for putting this together 馃憤

That seems like a lot of hassle for something so simple if you look how it is handled in AWS SAM etc?

@drissamri My comment is now long out of date and should not be used. CDK moves quickly.

@ryan-mars does it mean there's a native support in CDK now? as of 1.61.X?

I've shared my solution in this stackoverflow answer. It works without deploying twice.
Maybe @ryan-mars knows an even cleaner way?

I have also published this openapigateway construct for Python to abstract away some complexity.

@suud Your StackOverflow answer is exactly what I'm doing with JS now.

@zacyang There's currently no support for OpenAPI in the @aws-cdk/aws-apigatewayv2.HttpApi construct like there is in @aws-cdk_aws-apigateway.SpecRestApi.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

abelmokadem picture abelmokadem  路  3Comments

nzspambot picture nzspambot  路  3Comments

peterdeme picture peterdeme  路  3Comments

v-do picture v-do  路  3Comments

Kent1 picture Kent1  路  3Comments