swagger AWS CDK如何从OpenApi规范中创建一个由Lambda支持的API网关?

vvppvyoh  于 2023-01-30  发布在  其他
关注(0)|答案(4)|浏览(155)

我想使用AWS CDK来定义一个API网关和一个APIG将代理到的lambda。
OpenAPI规范支持Swagger规范的x-amazon-apigateway-integration自定义扩展(详细的here),为此需要lambda的调用URL。如果lambda与API定义在同一个堆栈中,我不知道如何在OpenAPI规范中提供这一点。我能想到的最好的方法是定义一个堆栈,其中包含lambda,然后从中获取输出,并运行sed在OpenAPI规范中执行查找和替换操作以插入URI,然后使用此修改后的OpenAPI规范创建第二个堆栈。
示例:

/items:
    post:
      x-amazon-apigateway-integration:
        uri: "arn:aws:apigateway:eu-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:eu-west-2:123456789012:function:MyStack-SingletonLambda4677ac3018fa48679f6-B1OYQ50UIVWJ/invocations"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        type: "aws_proxy"

这似乎是一个先有鸡还是先有蛋的问题,以上是唯一的方法吗?
我尝试使用SpecRestApi CDK构造的defaultIntegration属性。文档声明:
除非指定了集成,否则用作此API中创建的所有方法的默认值的集成。
这看起来像是a应该能够使用CDK规范中定义的lambda来定义一个默认的集成,从而让所有的方法都使用这个集成,而不需要事先知道lambda的uri。
因此我试着这样做:

SingletonFunction myLambda = ...

SpecRestApi openapiRestApi = SpecRestApi.Builder.create(this, "MyApi")
                        .restApiName("MyApi")
                        .apiDefinition(ApiDefinition.fromAsset("openapi.yaml"))
                        .defaultIntegration(LambdaIntegration.Builder.create(myLambda)
                                    .proxy(false)
                                    .build())
                        .deploy(true)
                        .build();

openapi.yaml中定义的OpenAPI规范不包括x-amazon-apigateway-integration节;它只有一个在标准OpenApi3规范中定义的GET方法。
但是,当我尝试部署它时,我得到一个错误:

No integration defined for method (Service: AmazonApiGateway; Status Code: 400; Error Code: BadRequestException; Request ID: 56113150-1460-4ed2-93b9-a12618864582)

这似乎是一个bug,所以我提交了一个here
Q2.如何使用CDK定义API网关和Lambda,并通过OpenAPI规范将两者连接在一起?

vi4fp9gy

vi4fp9gy1#

有一个现有的解决方法。方法如下:
您的OpenAPI文件必须如下所示:

openapi: "3.0.1"
info:
  title: "The Super API"
  description: "API to do super things"
  version: "2019-09-09T12:56:55Z"

servers:
- url: ""
  variables:
    basePath:
      default:
        Fn::Sub: ${ApiStage}

paths:
  /path/subpath:
    get:
      parameters:
      - name: "Password"
        in: "header"
        schema:
          type: "string"
      responses:
        200:
          description: "200 response"
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/UserConfigResponseModel"
      security:
      - sigv4: []
      x-amazon-apigateway-integration:
        uri: 
          Fn::Sub: "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${MySuperLambda.Arn}/invocations"
        responses:
          default:
            statusCode: "200"
        requestTemplates:
          application/json: "{blablabla}"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        type: "aws"

如您所见,这个OpenAPI模板引用了ApiStageAWS::RegionMySuperLambda.Arn
关联的cdk文件包含以下内容:

// To pass external string, nothing better than this hacky solution: 
const ApiStage = new CfnParameter(this, 'ApiStage',{type: 'String', default: props.ApiStage})
ApiStage.overrideLogicalId('ApiStage')

这里的ApiStage是用在props中的。它允许我在CI期间用一个环境变量将其传递给cdk应用程序。

const MySuperLambda = new lambda.Function(this, 'MySuperLambda', {
    functionName: "MySuperLambda",
    description: "Hello world",
    runtime: lambda.Runtime.PYTHON_3_7,
    code: lambda.Code.asset(lambda_asset),
    handler: "MySuperLambda.lambda_handler",
    timeout: cdk.Duration.seconds(30),
    memorySize: 128,
    role: MySuperLambdaRole
  });

  const forceLambdaId = MySuperLambda.node.defaultChild as lambda.CfnFunction
  forceLambdaId.overrideLogicalId('MySuperLambda')

在这里,和前面一样,我强制CDK覆盖逻辑id,以便在部署之前知道id,否则,cdk会给逻辑id添加一个后缀。

const asset = new Asset(this, 'SampleAsset', {
    path: './api-gateway-definitions/SuperAPI.yml',
  });

这允许我将OpenAPI文件直接上传到cdk存储桶中(无需创建新的存储桶,这太棒了)。

const data = Fn.transform('AWS::Include', {'Location': asset.s3ObjectUrl})

这是云形成魔法的一部分。这是Fn::Sub和Fn::GetAtt被解释的地方。我无法让它和!Ref函数一起工作。

const SuperApiDefinition = apigateway.AssetApiDefinition.fromInline(data)

从以前读取的文件创建API定义。

const sftpApiGateway = new apigateway.SpecRestApi(this, 'superAPI', {
    apiDefinition: SuperApiDefinition,
    deploy: false
  })

最后,创建SpecRestApi. Run and magic,这是有效的.你可能仍然会遇到400个错误,可能是因为你的OpenAPI文件格式不正确(不要使用!Ref).
我会推荐这个吗?嗯。这几乎是一个变通方案。如果你想在你的CI中使用OpenAPI格式的动态变量,它真的很有用。不需要太多的努力,你可以在dev和prod中部署,只需要切换一个环境变量。
然而,这感觉真的很笨拙,似乎不符合CDK的理念。这是我目前用于部署的,但将来可能会改变。我相信一个真正的模板解决方案可能会更适合这里,但现在,我真的没有考虑过它。

g6ll5ycj

g6ll5ycj2#

我提出了一个解决方案,它比这里的其他答案简单一些,因为它不需要阶段变量或多个部署。
首先,将x-amazon-apigateway-integrationuri设置为类似${API_LAMBDA_ARN}的变量,并使用与此示例中相同的typehttpMethod

[...]
  "paths": {
    "/pets": {
      "get": {
        "summary": "List all pets",
        "responses": {
          [...]
        },
        "x-amazon-apigateway-integration": {
          "uri": "${API_LAMBDA_ARN}",
          "type": "AWS_PROXY",
          "httpMethod": "POST",
        }
      }
    }
  },
[...]

然后,您可以使用此构造(或等效的TypeScript实现)在构建时替换变量,并基于OpenAPI文档创建API Gateway Http API:

from aws_cdk import (
    core,
    aws_iam as iam,
    aws_lambda as _lambda,
    aws_apigatewayv2 as apigateway
)

class OpenApiLambdaStack(core.Stack):
    def __init__(
        self, scope: core.Construct, construct_id: str, **kwargs
    ) -> None:
        super().__init__(scope, construct_id, **kwargs)

        # function that handles api request(s)
        api_lambda = _lambda.Function([...])

        # read openapi document
        with open("openapi.json", "r") as json_file:
            content = json_file.read()
        # replace the variable by the lambda functions arn
        content = content.replace("${API_LAMBDA_ARN}", api_lambda.function_arn)
        openapi = json.loads(content)

        # create apigateway
        http_api = apigateway.HttpApi(self, "OpenApiLambdaGateway")
        # use escape hatches to import OpenAPI Document
        # see: https://docs.aws.amazon.com/cdk/latest/guide/cfn_layer.html
        http_api_cfn: apigateway.CfnApi = http_api.node.default_child
        http_api_cfn.add_property_override("Body", openapi)
        http_api_cfn.add_property_deletion_override("Name")
        http_api_cfn.add_property_deletion_override("ProtocolType")
        # let it fail on warnings to be sure everything went right
        http_api_cfn.add_property_override("FailOnWarnings", True)

        # construct arn of createad api gateway (to grant permission)
        http_api_arn = (
            f"arn:{self.partition}:execute-api:"
            f"{http_api.env.region}:{http_api.env.account}:"
            f"{http_api.http_api_id}/*/*/*"
        )

        # grant apigateway permission to invoke api lambda function
        api_lambda.add_permission(
            f"Invoke By {http_api.node.id} Permission",
            principal=iam.ServicePrincipal("apigateway.amazonaws.com"),
            action="lambda:InvokeFunction",
            source_arn=http_api_arn,
        )
        
        # output api gateway url
        core.CfnOutput(self, "HttpApiUrl", value=http_api.url)

Python用户可能也会对我发布的openapigateway结构感兴趣,它支持JSON和YAML,使这个过程更加简单。

fivyi3re

fivyi3re3#

看起来我所追求的是由this CDK issue跟踪的。同时,我在这里对这个问题的评论的指导下提出了一个解决方案。
我使用https://github.com/spullara/mustache.java来解析我的OpenAPI规范文件,并替换其中引用API网关的调用ARN(API网关本身引用Lambda ARN)的模板值。

Map<String, Object> variables = new HashMap<>();
variables.put("restapi-lambda", String.format("arn:aws:apigateway:%s:lambda:path/2015-03-31/functions/%s/invocations", props.getEnv().getRegion(), myLambda.getFunctionArn()));

Writer writer = new StringWriter();
MustacheFactory mf = new DefaultMustacheFactory();

Object openapiSpecAsObject;
try (Reader reader = new FileReader(new File("myapi.yaml"))) {
    Mustache mustache = mf.compile(reader, "OAS");
    mustache.execute(writer, scopes);
    writer.flush();

    ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
    openapiSpecAsObject = yamlMapper.readValue(writer.toString(), Object.class);

}

SpecRestApi openapiRestApi = SpecRestApi.Builder.create(this, "MyRestApi")
                                                .restApiName("MyRestApi")
                                                .apiDefinition(ApiDefinition.fromInline(openapiSpecAsObject))
                                                .deploy(true)
                                                .build();

注意,props是引用Stack prop 的变量,myLambda是对SingletonFunction的引用。
我的OpenAPI规范如下所示(删除了标题和模型部分):

paths:
  /items:
    get:
      summary: List all items.
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ItemList'
      x-amazon-apigateway-integration:
        uri: "{{restapi-lambda}}"
        passthroughBehavior: "when_no_match"
        httpMethod: "POST"
        type: "aws_proxy"

还请注意,当我授予API Gateway权限以调用lambda时,如下所示:

myLambda.grantInvoke(ServicePrincipal.Builder.create("apigateway.amazonaws.com")
                                              .build());

我仍然收到一个500错误,并且在日志中看到一个“Invalid permissions on Lambda function”错误消息。如果我向Lambda添加权限,如下所示:

myLambda.addPermission("PermitAPIGInvocation", Permission.builder()
                                  .action("lambda:InvokeFunction")
                                  .principal(ServicePrincipal.Builder.create("apigateway.amazonaws.com")
                                     .build())
                                  .sourceArn(openapiRestApi.arnForExecuteApi())
                                  .build());

那么我现在需要在权限生效之前重新部署API。我仍在努力避免这种情况。

2izufjch

2izufjch4#

我最近仅使用Python和CDK实现了相同的功能,如下所示

from aws_cdk import (
    aws_apigateway as apig,
    aws_lambda,
    Stack,
    BundlingOptions,
    aws_s3_assets as s3_assets,
    Fn
)
from constructs import Construct

from os import path

import json

class ApigLambdaStack(Stack):

    def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        my_lambda_fn = aws_lambda.Function(self, 'MyFunction',
            runtime=aws_lambda.Runtime.PYTHON_3_9,
            handler='main.handler',            
            code=aws_lambda.Code.from_asset(path.join(path.dirname('.'), 'my_lambda_fn'),
                bundling=BundlingOptions(
                    image=aws_lambda.Runtime.PYTHON_3_9.bundling_image,
                        command=['bash', '-c', 'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output'
                    ]
                )
            )
        )

        swagger_file = None
        with open('./swagger.json') as f:
            swagger_file = f.read()

        swagger_file = swagger_file.replace("${LAMBDA_ARN}", my_lambda_fn.function_arn)                                    

        my_apig = apig.SpecRestApi(self, 'my-awesome-apis', api_definition=apig.ApiDefinition.from_inline(json.loads(swagger_file)))

在我的swagger.json文件里我已经

{
    "openapi" : "3.0.1",
    "info" : {
      "title" : "demo",
      "version" : "2023-01-26T11:14:54Z"
    },
    "servers" : [ {
      "url" : "https://12345abc.execute-api.eu-west-1.amazonaws.com/{basePath}",
      "variables" : {
        "basePath" : {
          "default" : "/v1"
        }
      }
    } ],
    "paths" : {
      "/" : {
        "post" : {
          "responses" : {
            "200" : {
              "description" : "200 response",
              "content" : {
                "application/json" : {
                  "schema" : {
                    "$ref" : "#/components/schemas/Empty"
                  }
                }
              }
            }
          },
          "x-amazon-apigateway-integration" : {
            "type" : "aws_proxy",
            "uri" : "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LAMBDA_ARN}/invocations",
            "httpMethod" : "POST",
            "responses" : {
              "default" : {
                "statusCode" : "200"
              }
            },
            "passthroughBehavior" : "when_no_match",
            "contentHandling" : "CONVERT_TO_TEXT"
          }
        }
      }
    },
    "components" : {
      "schemas" : {
        "Empty" : {
          "title" : "Empty Schema",
          "type" : "object"
        }
      }
    }
  }

注意:要明确的是,LAMBDA_ARN是一个占位符,它将被CloudFormation而不是Python所取代! Python对Lambda ARN一无所知...
希望这有帮助!

相关问题