【问题标题】:AWS CDK how to create an API Gateway backed by Lambda from OpenApi spec?AWS CDK 如何从 OpenApi 规范创建由 Lambda 支持的 API 网关?
【发布时间】:2020-09-22 13:37:42
【问题描述】:

我想使用 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,除非指定了集成。

这似乎应该能够使用 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 节;它只是在标准 OpenApi 3 规范中定义了一个 GET 方法。

但是,当我尝试部署它时,我收到一个错误:

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

这似乎是一个错误,所以我提交了一个here

第二季度。如何使用 CDK 定义 API 网关和 Lambda 并通过 OpenAPI 规范将两者连接在一起?

【问题讨论】:

    标签: amazon-web-services aws-lambda swagger aws-api-gateway openapi


    【解决方案1】:

    看起来我的目标是由this CDK issue 跟踪。与此同时,我根据关于该问题的评论 here 提出了解决方法。

    我使用 https://github.com/spullara/mustache.java 解析我的 OpenAPI 规范文件并替换其中引用 API 网关的调用 ARN(它本身引用 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 属性的变量,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 错误,并且在日志中我可以看到“Lambda 函数的权限无效”错误消息。如果我向 Lambda 添加权限,如下所示:

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

    那么我目前需要在权限生效之前重新部署 API。我仍在研究如何避免这种情况。

    【讨论】:

    • 但是 lambda 的 arn 仅在部署期间可用,对吧?
    【解决方案2】:

    有一个现有的解决方法。方法如下:

    您的 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 用于道具。例如,它允许我在 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})
    

    这是 Cloudformation 魔法的一部分。这是解释 Fn::Sub 和 Fn::GetAtt 的地方。我无法让它与 !Ref 函数一起使用。

    const SuperApiDefinition = apigateway.AssetApiDefinition.fromInline(data)
    

    从之前读取的文件中创建一个 api 定义。

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

    最后,创建 SpecRestApi。 跑步和魔术,这是有效的。您可能仍会遇到 400 错误,这可能是因为您的 OpenAPI 文件格式不正确(并且不要使用 !Ref)。

    我会推荐这个吗? 嗯。 这几乎是一种解决方法。如果您想在 CI 中使用带有动态变量的 OpenAPI 格式,这非常有用。无需太多努力,您只需切换 1 个环境变量即可在 dev 和 prod 中进行部署。

    但是,这感觉很老套,似乎不符合 CDK 的理念。这是我目前用于部署的,但将来可能会改变。我相信真正的模板解决方案可能更适合这里,但现在,我并没有真正考虑过。

    【讨论】:

      【解决方案3】:

      我想出了一个比这里的其他答案更简单的解决方案,因为它不需要阶段变量或多次部署。

      首先,将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。

      【讨论】:

      • 但是 lambda 的 arn 仅在部署期间可用,对吧?
      猜你喜欢
      • 1970-01-01
      • 2020-02-17
      • 2018-08-15
      • 1970-01-01
      • 1970-01-01
      • 2021-02-03
      • 1970-01-01
      • 1970-01-01
      • 2019-12-12
      相关资源
      最近更新 更多