Build & Test on GitHub

language language language platform platform license

Swift OpenAPI 的 AWS Lambda 传输

此库为 AWS Lambda transport for Swift OpenAPI generator 提供了一个 AWS Lambda 传输

此库允许将 Swift OpenAPI generator 生成的服务器端 Swift OpenAPI 实现公开为 AWS Lambda 函数。

此库提供两个功能

也支持其他 Lambda 函数绑定(事件类型),具体取决于您的需求。 我们包含有关创建与 Amazon API Gateway (REST API 模式) 绑定的说明

前提条件

要基于 OpenAPI API 定义编写和部署 AWS Lambda 函数,您需要以下内容

概括

如果您已经有一个 OpenAPI 定义,您已经生成了服务器存根,并且编写了一个实现,那么以下是将您的 OpenAPI 服务实现公开为 AWS Lambda 函数和 Amazon API Gateway HTTP API (又名 APIGatewayV2) 的附加步骤。

如果您不知道如何开始,请阅读下一节,其中有一个 包含分步说明的教程

要将您的 OpenAPI 实现公开为 AWS Lambda 函数

  1. 将依赖项添加到您的 Package.swift

    项目依赖项

  dependencies: [
    .package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.0.0"),
    .package(url: "https://github.com/apple/swift-openapi-runtime.git", from: "1.0.0"),
    
    // add these three dependencies
    .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha.1"),
    .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.1.0"),
    .package(url: "https://github.com/sebsto/swift-openapi-lambda", from: "0.1.1")
  ],

目标依赖项

    .executableTarget(
      name: "YourOpenAPIService",
      dependencies: [
        .product(name: "OpenAPIRuntime",package: "swift-openapi-runtime"),
        
        // add these three dependencies
        .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
        .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
        .product(name: "OpenAPILambda",package: "swift-openapi-lambda"),
      ],
  1. 将协议和构造函数添加到您现有的 OpenAPI 服务实现中

只需对您现有的实现进行四处更改

Animated GIF to show the changes

import Foundation
import OpenAPIRuntime
import OpenAPILambda // <-- 1. import this library 

@main // <-- 2. flag this struct as the executable target entrypoint
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi { // <-- 3. add the OpenAPILambdaHttpApi protocol 

  init(transport: OpenAPILambdaTransport) throws { // <-- 4. add this constructor (don't remove the call to `registerHandlers(on:)`)
    try self.registerHandlers(on: transport)
  }

  // the rest below is unmodified 

  func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {

    let symbol = input.path.symbol

    let price = Components.Schemas.quote(
        symbol: symbol,
        price: Double.random(in: 100..<150).rounded(),
        change: Double.random(in: -5..<5).rounded(),
        changePercent: Double.random(in: -0.05..<0.05),
        volume: Double.random(in: 10000..<100000).rounded(),
        timestamp: Date())

    return .ok(.init(body: .json(price)))
  }
}
  1. 打包和部署您的 Lambda 函数 + 创建一个 HTTP API Gateway (又名 APIGatewayV2)

🎉 尽情享用吧!

教程(股票报价 API 服务示例快速入门)

第 1 部分 - 代码

  1. 创建一个 Swift 可执行项目
mkdir quoteapi && cd quoteapi
swift package init --name quoteapi --type executable
  1. 以 YAML 或 JSON 格式编写或导入 OpenAI API 定义
#
# the $ signs are escaped (\$) to work with the cat << EOF command
# if you choose to copy the content directly to a text editor,
# be sure to remove the \ (that means \$ becomes $)
#
cat << EOF > Sources/openapi.yaml
openapi: 3.1.0
info:
  title: StockQuoteService
  version: 1.0.0
  
components:
  schemas:
    quote:
      type: object
      properties:
        symbol:
          type: string
        price:
          type: number
        change:
          type: number
        changePercent:
          type: number
        volume:
          type: number
        timestamp:
          type: string
          format: date-time
          
paths:
  /stocks/{symbol}:
    get:
      summary: Get the latest quote for a stock
      operationId: getQuote
      parameters:
        - name: symbol
          in: path
          required: true
          schema:
            type: string
      tags:
        - stocks
      responses:
        200:
          description: OK
          content:
            application/json:
              schema:
                \$ref: '#/components/schemas/quote'
        400:
          description: Bad Request
        404:
          description: Not Found
EOF
  1. 添加一个 Swift OpenAPI generator 配置文件以仅生成服务器端
cat << EOF > Sources/openapi-generator-config.yaml
generate:
  - types
  - server
EOF
  1. 使用此 Package.swift 文件定义目标及其依赖项
cat << EOF > Package.swift
// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
  name: "QuoteService",
  platforms: [
    .macOS(.v13), .iOS(.v15), .tvOS(.v15), .watchOS(.v6),
  ],
  products: [
    .executable(name: "QuoteService", targets: ["QuoteService"]),
  ],
  dependencies: [
    .package(url: "https://github.com/apple/swift-openapi-generator.git", from: "1.0.0"),
    .package(url: "https://github.com/apple/swift-openapi-runtime.git", from: "1.0.0"),
    .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", from: "1.0.0-alpha.1"),
    .package(url: "https://github.com/swift-server/swift-aws-lambda-events.git", from: "0.1.0"),
    .package(url: "https://github.com/sebsto/swift-openapi-lambda", from: "0.1.1") 
  ],
  targets: [
    .executableTarget(
      name: "QuoteService",
      dependencies: [
        .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
        .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
        .product(name: "OpenAPIRuntime",package: "swift-openapi-runtime"),
        .product(name: "OpenAPILambda",package: "swift-openapi-lambda"),
      ],
      path: "Sources",
      resources: [ 
        .copy("openapi.yaml"),
        .copy("openapi-generator-config.yaml")
      ],
      plugins: [
        .plugin(
            name: "OpenAPIGenerator",
            package: "swift-openapi-generator"
        )
      ]  
    ),
  ]
)
EOF
  1. 生成 OpenAPI API 定义的服务器端 Swift 存根
swift build
  1. main.swift 替换为您自己的实现
rm Sources/main.swift
cat << EOF > Sources/QuoteService.swift
import Foundation
import OpenAPIRuntime
import OpenAPILambda

@main
struct QuoteServiceImpl: APIProtocol, OpenAPILambdaHttpApi {

  init(transport: OpenAPILambdaTransport) throws {
    try self.registerHandlers(on: transport)
  }

  func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {

    let symbol = input.path.symbol

    let price = Components.Schemas.quote(
        symbol: symbol,
        price: Double.random(in: 100..<150).rounded(),
        change: Double.random(in: -5..<5).rounded(),
        changePercent: Double.random(in: -0.05..<0.05),
        volume: Double.random(in: 10000..<100000).rounded(),
        timestamp: Date())

    return .ok(.init(body: .json(price)))
  }
}
EOF
  1. 构建项目以确保一切正常
swift build

第 2 部分 - 部署

  1. 以 Docker 文件和 Makefile 的形式添加 Lambda 构建说明。我们在 Amazon Linux 2 上为 Swift 5.9 构建
#
# the $ signs are escaped (\$) to work with the cat << EOF command
# if you choose to copy the content directly to a text editor,
# be sure to remove the \ (that means \$ becomes $)
#
cat << EOF > Dockerfile
# image used to compile your Swift code
FROM public.ecr.aws/docker/library/swift:5.9.1-amazonlinux2
RUN yum -y install git jq tar zip openssl-devel
EOF

cat << EOF > Makefile
### Add functions here and link them to builder-bot format MUST BE "build-FunctionResourceName in template.yaml"

build-QuoteService: builder-bot

# Helper commands
deploy:
	sam deploy 

logs:
	sam logs --stack-name QuoteService --name QuoteService 

tail:
	sam logs --stack-name QuoteService --name QuoteService --tail

######################  No Change required below this line  ##########################

builder-bot:
	\$(eval \$@PRODUCT = \$(subst build-,,\$(MAKECMDGOALS)))
	\$(eval \$@BUILD_DIR = \$(PWD)/.aws-sam/build-swift)
	\$(eval \$@STAGE = \$(\$@BUILD_DIR)/lambda)
	\$(eval \$@ARTIFACTS_DIR = \$(PWD)/.aws-sam/build/\$(\$@PRODUCT))
	
	# build docker image to compile Swift for Linux
	docker build -f Dockerfile . -t swift-builder

	# prep directories
	mkdir -p \$(\$@BUILD_DIR)/lambda \$(\$@ARTIFACTS_DIR)

	# compile application inside Docker image using source code from local project folder
	docker run --rm -v \$(\$@BUILD_DIR):/build-target -v \`pwd\`:/build-src -w /build-src swift-builder bash -cl "swift build --static-swift-stdlib --product \$(\$@PRODUCT) -c release --build-path /build-target"
	
	# create lambda bootstrap file
	docker run --rm -v \$(\$@BUILD_DIR):/build-target -v \`pwd\`:/build-src -w /build-src swift-builder bash -cl "cd /build-target/lambda && ln -s \$(\$@PRODUCT) /bootstrap"
  
	# copy binary to stage
	cp \$(\$@BUILD_DIR)/release/\$(\$@PRODUCT) \$(\$@STAGE)/bootstrap
  
  	# copy app from stage to artifacts dir
	cp \$(\$@STAGE)/* \$(\$@ARTIFACTS_DIR)
   
EOF
  1. 添加一个 SAM 模板以部署 Lambda 函数和 API Gateway
#
# the $ signs are escaped (\$) to work with the cat << EOF command
# if you choose to copy the content directly to a text editor,
# be sure to remove the \ (that means \$ becomes $)
#
cat << EOF > template.yml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: SAM Template for QuoteService

Globals:
  Function:
    Timeout: 60
    CodeUri: .
    Handler: swift.bootstrap
    Runtime: provided.al2
    MemorySize: 512
    Architectures:
      - arm64

Resources:
  # Lambda function
  QuoteService:
    Type: AWS::Serverless::Function
    Properties:
      Events:
        # pass through all HTTP verbs and paths
        Api:
          Type: HttpApi
          Properties:
            Path: /{proxy+}
            Method: ANY

    Metadata:
      BuildMethod: makefile

# print API endpoint and name of database table
Outputs:
  SwiftAPIEndpoint:
    Description: "API Gateway endpoint URL for your application"
    Value: !Sub "https://\${ServerlessHttpApi}.execute-api.\${AWS::Region}.amazonaws.com"
EOF
  1. 为 Amazon Linux 2 构建 Lambda 函数可执行文件
sam build
  1. 部署 Lambda 函数并在其前面创建一个 API Gateway
# use --guided for the first deployment only.
# SAM cli collects a few parameters and store them in `samconfig.toml`

sam deploy --guided --stack-name QuoteService

接受所有默认值,除了

QuoteService has no authentication. Is this okay? [y/N]: <-- answer Y here 

此命令输出 API Gateway 的 URL,例如

Outputs                                                                                                                     
-----------------------------------------------------------------------------------------------------------------------------
Key                 SwiftAPIEndpoint                                                                                        
Description         API Gateway endpoint URL for your application                                                           
Value               https://747ukfmah7.execute-api.us-east-1.amazonaws.com                                                  
-----------------------------------------------------------------------------------------------------------------------------
  1. 测试您的设置
curl [[ Replace with SWIFTAPIEndpoint value ]]/stocks/AAPL
{
  "change" : -4,
  "changePercent" : -0.030052760210257923,
  "price" : 111,
  "symbol" : "AAPL",
  "timestamp" : "2023-12-13T03:12:35Z",
  "volume" : 63812
}

部署成本

在新的 AWS 账户上,部署和测试此示例代码不收取任何费用,每月最多 100 万次调用。它属于 AWS LambdaAmazon API Gateway 的永久 AWS 免费套餐

当您的账户超过一年时,您将被收取每百万次 API Gateway 调用 1.0 美元的费用。AWS Lambda 函数调用在每月最多 400 万次调用和 400,000 GB 秒计算时间内仍然免费。

清理

要删除 AWS Lambda 函数、API Gateway 以及使用 sam 创建的角色和权限,只需键入

sam delete

本地测试

git clone https://github.com/sebsto/swift-openapi-lambda.git && cd swift-openapi-lambda
# In the directory of the Swift OpenAPI Lambda transport project
LOCAL_LAMBDA_SERVER_ENABLED=true swift run 

# from another terminal, in the directory of the QuoteAPI sample project
curl -v -X POST --header "Content-Type: application/json" --data @events/GetQuote.json  http://127.0.0.1:7000/invoke

实现您自己的 OpenAPILambda 以支持其他 AWS Lambda 事件类型

当您将 AWS Lambda 函数公开给其他事件类型时,您必须专门化 OpenAPILambda 协议并实现两种方法,这两种方法将您的 Lambda 函数输入数据转换为 OpenAPIRequest,反之亦然,将 OpenAPIResponse 转换为您的 Lambda 函数输出类型。

这是一个 Amazon API Gateway (Rest Api)(又名原始 API Gateway)的示例。

我从一个 OpenAPI 存根实现开始 - 未修改。

import Foundation
import OpenAPIRuntime

struct QuoteServiceImpl: APIProtocol {
  func getQuote(_ input: Operations.getQuote.Input) async throws -> Operations.getQuote.Output {

    let symbol = input.path.symbol

    let price = Components.Schemas.quote(
        symbol: symbol,
        price: Double.random(in: 100..<150).rounded(),
        change: Double.random(in: -5..<5).rounded(),
        changePercent: Double.random(in: -0.05..<0.05),
        volume: Double.random(in: 10000..<100000).rounded(),
        timestamp: Date())

    return .ok(.init(body: .json(price)))
  }
}

接下来,我实现一个符合 OpenAPILambda 的结构体。此结构体定义

这是一个使用 APIGatewayRequestAPIGatewayResponse 的示例

@main
struct QuoteServiceLambda: OpenAPILambda {
  typealias Event = APIGatewayRequest
  typealias Output = APIGatewayResponse
  public init(transport: OpenAPILambdaTransport) throws {
    let openAPIHandler = QuoteServiceImpl()
    try openAPIHandler.registerHandlers(on: transport)
  }
}

下一步是从 OpenAPILambda 协议实现两种方法,以将您的 Lambda 函数输入数据 (APIGatewayRequest) 转换为 OpenAPIRequest,反之亦然,将 OpenAPIResponse 转换为您的 Lambda 函数输出类型 (APIGatewayResponses)。

extension OpenAPILambda where Event == APIGatewayRequest {
    /// Transform a Lambda input (`APIGatewayRequest` and `LambdaContext`) to an OpenAPILambdaRequest (`HTTPRequest`, `String?`)
    public func request(context: LambdaContext, from request: Event) throws -> OpenAPILambdaRequest {
        (try request.httpRequest(), request.body)
    }
}

extension OpenAPILambda where Output == APIGatewayResponse {
    /// Transform an OpenAPI response (`HTTPResponse`, `String?`) to a Lambda Output (`APIGatewayResponse`)
    public func output(from response: OpenAPILambdaResponse) -> Output {
        var apiResponse = APIGatewayResponse(from: response.0)
        apiResponse.body = response.1
        return apiResponse
    }
}

为了使上面的代码简短、简单且易于阅读,我们建议在 Lambda 源事件类型上实现任何扩展。以下是支持上述代码所需的扩展。这些是从一种类型到另一种类型的简单数据转换方法。

extension APIGatewayRequest {
    
    /// Return an `HTTPRequest.Method` for this `APIGatewayRequest`
    public func httpRequestMethod() throws -> HTTPRequest.Method {
        guard let method = HTTPRequest.Method(rawValue: self.httpMethod.rawValue) else {
            throw OpenAPILambdaHttpError.invalidMethod(self.httpMethod.rawValue)
        }
        return method
    }

    /// Return an `HTTPRequest` for this `APIGatewayV2Request`
    public func httpRequest() throws -> HTTPRequest {
        try HTTPRequest(
            method: self.httpRequestMethod(),
            scheme: "https",
            authority: "",
            path: self.path,
            headerFields: self.headers.httpFields()
        )
    }
}

extension APIGatewayResponse {
    
    /// Create a `APIGatewayV2Response` from an `HTTPResponse`
    public init(from response: HTTPResponse) {
        self = APIGatewayResponse(
            statusCode: .init(code: UInt(response.status.code)),
            headers: .init(from: response.headerFields),
            isBase64Encoded: false
        )
    }
}

您可以应用相同的设计来支持其他 AWS Lambda 事件类型。但是,请记住,OpenAPILambda 实现严重偏向于接收、路由和响应 HTTP 请求。

参考

Swift OpenAPI generator

要开始使用 Swift OpenAPI generator,请查看完整的文档,其中包含 一个分步教程

AWS Lambda 上的 Swift

AWS Lambda 的 Swift 运行时允许您使用 Swift 编程语言编写 AWS Lambda 函数。

要开始使用,请查看此分步教程文档

无服务器应用程序模型 (SAM)

阅读“什么是 SAM”以了解并开始使用 SAM。