Reference to a external swagger api definition

Hello,
I would like to reference inside the serverless.yml to an external swagger api definition.

Swagger is a widely spread way to define an api and it’s also supported from the api gateway to import external swagger definitions.

With a swagger api definition there can also be added aws features like api validation and custom api resonses to the api gateway, which will replace a lot of small serverless plugins with official aws features. Also, it gives a developer the possibility to use his api definition from other projects directly with the serverless framework and convert the project easier to serverless.

My current approach is to overwrite the serverless api with the aws put-rest-api command, but it’s not the optimal way to use my own api definition.

A similar feature was also requested in this post " Swagger and Serverless in parallel - bad idea?" but there was no solution and the there mentioned plugin looks like, that it will not be developed further.

It would be nice if I can define an api-definition path directly to the serverless resources, which would be used than to create the api-gateway.

2 Likes
  1. The Serverless Framework uses CloudFormation to handle deployments.
  2. CloudFormation supports Swagger (see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-apigateway-restapi.html)
  3. You can override the default CloudFormation template generated by the Serverless Framework (see https://serverless.com/framework/docs/providers/aws/guide/resources#override-aws-cloudformation-resource)

I would start by trying something like:

resources:
  Resources:
    ApiGatewayRestApi:
      Properties:
        Body:
          # Your swagger definition in YAML format goes here
4 Likes

Your solution seems to be exactly that I was looking for.
I also have successfully tested your approach with this:

resources:
    Resources:
      ApiGatewayRestApi:
        Type: 'AWS::ApiGateway::RestApi'
        Properties:
          Name: ${self:provider.apiName}-${self:provider.stage}
          Body:
            ${file(api-model/api.yml)}
      ApiGatewayDeployment:
        Type: AWS::ApiGateway::Deployment
        Properties:
          RestApiId:
            Ref: ApiGatewayRestApi
          StageName: ${self:provider.stage}

This import my api.yml like expected but it also removes a lot of advantages, the serverless framework gives me.
If I replace the whole API template with my API definition, I also have to define all other API ressources like the API keys, the API deployment and the trigger for the lambda functions by my self.

For me, an better solution would be, if I could use something like the put-rest-api command in cloudformation to overwrite only the Methods and Models from the serverless API template and keep the API Keys and lambda trigger.

aws apigateway put-rest-api --rest-api-id {AWS::ApiGateway::RestApi}
–region {self:provider.stage}
–mode overwrite
–body file://api-model/api.yml

2 Likes

Thanks for moving this discussion forward.
My organisation is API therefore Swagger first, rather than Function therefore Serverless first.
If we can achieve the successful marriage of the two (Swagger and Serverless) then I will win them over with Serverless, without it reluctantly I’m likely destined for Swagger/SAM.
The Swagger use case the organisation likes is the ability to forward engineer the consumer/client.

You don’t need to replace the whole template. The Serverless Framework will merge your resources section with the one it generates. My example adds the Body property to the CloudFormation generated by the Serverless Framework.

Hi,

I’m testing your trick by including the following swagger definition in the resource section:

openapi: 3.0.0
info:
  title: Sample API
  description: Description
  version: 0.1.9
paths:
  /users:
    get:
      summary: Returns a list of users.
      description: Optional description
      responses:
        '200':
          description: A JSON array of user names
          content:
            application/json:
              schema:
                type: array
                items:
                  type: string

However, when I serverless deploy my API, I get the following error message:

 An error occurred: ApiGatewayDeployment1554726393541 - No integration defined for method (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: ...).

Should I add x-amazon-apigateway-integration ?
Is there a way that serverless still handles the integration layer for us ?

My second question is:
Using swagger, how does serverless map the routes from the swagger definition, to the JS methods ?
I mean the swagger equivalent to the handler key-value pair:

functions:
  todo:
    handler: handler.todo

Thanks

2 Likes

This works great except for when using the serverless-offline plugin. Does anyone have any strategy to combine the two in your project?

Hey @slaursenfl, did you ever work out a strategy for this?

@thomas-ama, I’m running into the same issue you are now having

I have defined by swagger documentation and included it in the Body attribute of the ApiGatewayRestApi Resource section, but am getting
An error occurred: ApiGatewayDeployment1554726393541 - No integration defined for method (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: ...).

Did you ever find a way to resolve this issue?

Late to the party here but this thread was very helpful. Some documentation on how Serverless merges the resources would be helpful - my google fu hasn’t found anything particularly substantial yet.

@LucasRudd , I ran into this as well when doing SAM. The x-amazon-apigateway-integration property can be added to a path and use intrinsic functions:

"x-amazon-apigateway-integration": {
  "uri": {
    "Fn::Sub": "arn:aws:apigateway:${AWS::Region}:lambda:/path/2015-01-31/functions/${MyLambda.Arn}/invocations"
  },
  "passthroughBehavior": "never",
  "httpMethod": "POST",
  "type": "AWS_PROXY"
}

Docs here: https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration.html

I have been trying to get this working also. If you define all of your API endpoints in your swagger/openAPI specification, and include the x-amazon-apigateway-integration properties to associate endpoints with lambda functions as Ashpabb suggests, then you would not have any http event definitions on your functions (in the “functions” section of serverless.yml), because the relationship between the lambda functions and the API endpoints is defined in the swagger/openAPI spec. Indeed, if you do define http events for functions with the same endpoint names as in the swagger spec then you’ll get a Another resource with the same parent already has this name error from Serverless upon deployment. But if you don’t have any http event definitions, then Serverless does not create an API for you, so you have to do it all yourself in Cloudformation:

  • create the API based on the swagger/openAPI spec
  • grant the API permission to execute the defined functions
  • deploy the API

@buggy you say that Serverless will merge your resources with the one it generates, but it seems to me that it won’t generate any API resources in this scenario. You could add a dummy http event to your functions in order to prompt Serverless to create the API, the permissions and the deployment, but that’d leave a dummy endpoint lying around. So I think @Dragonil was correct when stating that this approach requires the developer to define all the API resources themselves when using a swagger/openAPI specification.

Here is the Cloudformation that I added to serverless.yml to get it working with a swagger specification:

functions:
  fetchData:
    handler: fetchDataLambda.handler

resources:
  Resources:

    # Create the API based on the OpenAPI specification

    ApiGatewayRestApi:
      Type: 'AWS::ApiGateway::RestApi'
      Properties:
        Body:
          ${file(./openApiSpecification.yml)}

    # Deploy the API

    ApiGatewayDeployment:
      Type: AWS::ApiGateway::Deployment
      Properties:
        RestApiId:
          Ref: ApiGatewayRestApi
        StageName: ${self:provider.stage}

    # Grant permission for the API to call the lambda - note that lambda named "fetchData"
    # earlier in this file becomes "FetchDataLambdaFunction" in Cloudformation

    Permission:
      Type: AWS::Lambda::Permission
      Properties:
        FunctionName:
          Fn::GetAtt:
            - FetchDataLambdaFunction
            - Arn
        Action: lambda:InvokeFunction
        Principal: apigateway.amazonaws.com

And the relevant part of my swagger spec looks like this:

/fetch:
  get:
    summary: Returns some data
    responses:
      '200':
        description: OK
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: '#/components/schemas/Item'
    x-amazon-apigateway-integration:
      uri:
        Fn::Join:
          - ""
          - - "arn:aws:apigateway:"
            - Ref: AWS::Region
            - ":lambda:path/2015-03-31/functions/"
            - Fn::GetAtt: ["FetchDataLambdaFunction", "Arn"]
            - '/invocations'
      passthroughBehavior: "when_no_match"
      httpMethod: "POST"
      type: "aws_proxy"

I appreciate this question was asked a long time ago, and perhaps there is a better approach now. If so, I’d love to hear about it. There doesn’t seem to be a lot of documentation around how to get a swagger/openAPI spec working within a Serverless application.

2 Likes

This solution was the one I was looking for! Thank you very much.

As a complement, for those who are trying to add more than 1 lambda function, in your swagger.yaml you need to add the reference to any endpoint that implements a lambda with the “x-amazon-apigateway-integration” extention, and within your serverless.yaml you can specify another “Permission” key with a variant name, like “PermissionLambdaOne, PermissionLambdaTwo” and provide the lambda name for each lambda you are building.

Also I can say that I’ve tested plugins like “serverless-swagger-api” but it didn’t function as expected. We’ll appreciate if these plugins were officialy supported by serverless, to make API building much more powerfull.

@salvadornava01 Can you provide an example with more than one Lambda?

@dalejacques Of course! Lets say for example, that you have 2 lamdba functions in your serverless.yml functions, named “testOne” and “testTwo”:

functions:
  testOne:
    handler: handler.testOne
  testTwo:
    handler: handler.testTwo

Then you need to reference that functions in your permissions, like:

PermissionTestOneLambdaFunction:
  Type: AWS::Lambda::Permission
  Properties:
    FunctionName: !GetAtt TestOneLambdaFunction.Arn
    Action: lambda:InvokeFunction
    Principal: apigateway.amazonaws.com

PermissionTestTwoLambdaFunction:
  Type: AWS::Lambda::Permission
  Properties:
    FunctionName: !GetAtt TestTwoLambdaFunction.Arn
    Action: lambda:InvokeFunction
    Principal: apigateway.amazonaws.com

and then in your “swagger.yaml” use those “FunctionName” as follows:

x-amazon-apigateway-integration:
        uri:
          Fn::Join:
            - ""
            - - "arn:aws:apigateway:"
              - Ref: AWS::Region
              - ":lambda:path/2015-03-31/functions/"
              - Fn::GetAtt: [ "TestOneLambdaFunction", "Arn" ]
              - '/invocations'
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        type: "aws_proxy"

x-amazon-apigateway-integration:
        uri:
          Fn::Join:
            - ""
            - - "arn:aws:apigateway:"
              - Ref: AWS::Region
              - ":lambda:path/2015-03-31/functions/"
              - Fn::GetAtt: [ "TestTwoLambdaFunction", "Arn" ]
              - '/invocations'
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        type: "aws_proxy"

And thats it! Your functions in your serverless project will be matched with the definitions on your swagger.yaml

:slight_smile:

@salvadornava01 Tremendous! Thank-you! I’m going through this right now and you just saved me hours of work! Let me know if I can ever repay the favor!

1 Like

I’m glad to know that I can help you mate!

And don’t worry, we are here to contribute to the community!

Wish you happy coding! :smiley:

@salvadornava01 I’m trying this solution and encountered "The CloudFormation template is invalid: Template error: instance of Fn::GetAtt references undefined resource "

functions: 
  - myFunction: 
      handler: src/handler/updatePerson.handler

resources:
  Resources:
    Permission:
      Type: AWS::Lambda::Permission
      Properties:
       FunctionName:
      Fn::GetAtt:
        - myFunction
        - Arn
    Action: lambda:InvokeFunction
    Principal: apigateway.amazonaws.com

@jericromero
In Fn::GetAtt, you need to reference the normalized Resource Name for the function.
See here for sls docs on how the normalized names are structured.

In short, change:

Fn::GetAtt:
        - myFunction
        - Arn

To:

Fn::GetAtt:
        - MyFunctionLambdaFunction
        - Arn

If you overwrite the Body property of the API Gateway created by serverless in resources/extensions section, you will have all the definition of the swagger file included and you won’t have to create the API Gateway from scratch. The only bad thing is that you won’t be able to test locally.

functions:
  HelloFunction:
    handler: handler.hello
    description: Description
    events:
      - http: 
          path: ''
          method: get

resources:
  extensions:
    ApiGatewayRestApi:
      Properties:
        Body:
          ${file(./api-definition/open-api/swagger.yml)}
        Mode: overwrite
x-amazon-apigateway-request-validators:
  all:
    validateRequestBody: true
    validateRequestParameters: true
  params-only:
    validateRequestBody: false
    validateRequestParameters: true
  body-only:
    validateRequestBody: true
    validateRequestParameters: false
x-amazon-apigateway-request-validator: params-only


paths:
  /hello:
    get:
      parameters:
        - $ref: '#/components/parameters/randomQueryParameter'
      responses:
        '200':
          $ref: '#/components/responses/200_OK_response'
        '400':
          $ref: '#/components/responses/400_BadRequest'
      x-amazon-apigateway-integration:
        type: "aws_proxy"
        httpMethod: "POST"
        passthroughBehavior: "when_no_match"
        timeoutInMillis: 28000 #ms
        uri:
          Fn::Join:
            - ""
            - - "arn:aws:apigateway:"
              - Ref: AWS::Region
              - ":lambda:path/2015-03-31/functions/"
              - Fn::GetAtt: [ "HelloFunction", "Arn" ]
              - '/invocations'
        cacheKeyParameters:
          - method.request.querystring.randomparameter
        requestParameters:
          integration.request.querystring.randomparameter: method.request.querystring.randomparameter
        responses:
          "200":
            statusCode: "200"
          "400":
            statusCode: "400"