AWS CDK 如何根据 OpenApi 规范创建由 Lambda 支持的 API 网关?

Joh*_*ohn 10 amazon-web-services swagger aws-lambda aws-api-gateway openapi

我想使用 AWS CDK 来定义 API 网关和 APIG 将代理到的 lambda。

OpenAPI 规范支持x-amazon-apigateway-integration对 Swagger 规范的自定义扩展(在此处详细说明),为此需要 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"

Run Code Online (Sandbox Code Playgroud)

一季度。这似乎是一个鸡与蛋的问题,以上是唯一的方法吗?

我尝试使用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();
Run Code Online (Sandbox Code Playgroud)

中定义的 OpenAPI 规范openapi.yaml不包括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)
Run Code Online (Sandbox Code Playgroud)

这似乎是一个错误,所以我在这里提交了一个。

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

小智 12

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

您的 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"
Run Code Online (Sandbox Code Playgroud)

如您所见,此 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') 
Run Code Online (Sandbox Code Playgroud)

这里在 props 中使用了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')
Run Code Online (Sandbox Code Playgroud)

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

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

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

const data = Fn.transform('AWS::Include', {'Location': asset.s3ObjectUrl})
Run Code Online (Sandbox Code Playgroud)

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

const SuperApiDefinition = apigateway.AssetApiDefinition.fromInline(data)
Run Code Online (Sandbox Code Playgroud)

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

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

最后,创建 SpecRestApi。奔跑并施展魔法,这行得通。您可能仍然会遇到 400 错误,可能是因为 OpenAPI 文件中的格式不正确(并且不要使用 !Ref)。

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

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


Joh*_*ohn 1

看起来我所追求的内容是由这个 CDK 问题跟踪的。与此同时,我根据此处关于该问题的评论提出了解决方法。

我使用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();

Run Code Online (Sandbox Code Playgroud)

请注意,props是一个引用Stackprops 的变量,并且myLambda是对 a 的引用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"

Run Code Online (Sandbox Code Playgroud)

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

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

Run Code Online (Sandbox Code Playgroud)

我仍然收到 500 错误,并且在日志中我可以看到“Lambda 函数的权限无效”错误消息。如果我向 Lambda 添加权限,如下所示:

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

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