如何在Swift中创建一个API并将其部署到AWS Lambda中

228 阅读9分钟

在几乎每一个真正的移动应用中,我们都可能需要一个后台,在那里我们的业务逻辑将被处理。在大多数情况下,会有两个不同的团队,一个负责移动端,一个负责项目的后端。但是,如果我们,作为iOS开发者,可以用自己喜欢的语言编写自己的后端呢?让我们在本文中探讨一下,我们如何使用Swift AWS Lambda Runtime和AWS Lambda来实现这一目标。

什么是AWS Lambda?

简单来说,它是AWS的一个服务提供者,我们可以在其中运行我们的代码,而不需要配置和管理一个服务器。我们只需将我们的代码以压缩文件的形式上传,AWS就会自动完成服务器上所需的所有配置,使我们的软件可用。

这种方法与拥有专用服务器的主要区别之一,除了更简单的管理外,还在于如果在某些时候我们需要增加处理能力来扩大我们的应用程序,如果我们正确设计应用程序,AWS Lambda会自动为我们做这些。

你可以在这个链接中查看更多。

Swift AWS Lambda运行时

定制的AWS Lambda运行时基本上是一个库,负责在Lambda函数被调用时管理和执行它的代码。有了Swift AWS Lambda Runtime,我们现在就可以用Swift编写无服务器代码,并使其可以与AWS Lambda服务一起使用。

创建我们的HTTP API

在本教程中,我们将在Swift中创建一个简单的HTTP API,并通过API Gateway将其公开,API Gateway是AWS套件中的另一项服务,允许我们将Lambda函数作为HTTP端点公开。

前提条件

  • 安装有XCode
  • 有一个AWS账户。
  • 有一个Auth0账户。
  • 在你的机器上安装Docker来编译我们要上传到AWS的代码。

第1步:定义API

我们将创建一个简单的API来处理一个有三种操作的简单的todo列表。

  • POST /todoitem创建一个新的todo项目。
  • GET /todoitems返回列表中的所有项目。
  • GET /todoitems/:id返回列表中的一个特定项目。

为了简化事情,我们的TodoItem将只有一个id和一个描述。

struct ToDoItem {
    var id: Int
    let description: String
}

第2步:设置项目

我们需要做的下一件事是创建我们的项目。在这种情况下,我们需要创建一个新的Swift包。要做到这一点,我们可以打开Xcode,进入文件→新建→Swift包选项,并命名为ToDoList-API。我们也可以通过运行以下命令从控制台创建它。 $ swift package init --type executable.

一旦我们创建了我们的项目,让我们打开并修改我们的Package.swift文件,加入所有需要的信息。

// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "ToDoList-API",
    platforms: [
        .macOS(.v10_15)
    ],
    products: [
        .executable(name: "ToDoList-API", targets: ["ToDoList-API"]),
    ],
    dependencies: [
        .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", .upToNextMajor(from:"0.3.0")),
    ],
    targets: [
        .target(
            name: "ToDoList-API",
            dependencies: [
                .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
                .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-runtime"),
            ],
            resources: [
                .process("Config.plist")
            ]
        ),
    ]
)

一旦你保存该文件,Xcode将开始下载所有需要的资源和依赖。在这个例子中,我们将使用两个依赖项。

  • AWSLambdaRuntime来处理与AWS Lambda运行时API的通信。
  • AWSLambdaEvents来处理我们代码中与API网关的事件。

第3步:创建我们的第一个Lambda函数

我们现在必须开发我们的Lambda函数。要做到这一点,我们必须在 /Source目录中创建一个新文件,并将其命名为main.swift。在这个文件中,我们将通过调用Swift Lambda Runtime的 Lambda.run函数。这个函数需要一个输入和一个回调作为参数。如果操作成功,我们可以使用回调来返回我们想要的东西,否则就是错误。

对于Lambada收到的每一个调用,我们的Runtime将执行我们在该 Lambda.run函数。在这种情况下,我们将只接受一个字符串作为输入,并返回一个问候信息。

import AWSLambdaRuntime

struct Input: Codable {
  let name: String
}

struct Output: Codable {
  let greeting: String
}

Lambda.run { (context, input: Input, callback: @escaping (Result<Output, Error>) -> Void) in
  callback(.success(Output(greeting: "Hello \(input.name)")))
}

为了在我们的机器上运行我们的lambda,我们需要在方案的运行设置中添加一个自定义环境变量(LOCAL_LAMBDA_SERVER_ENABLED=true)到我们方案的运行设置中。这将在我们的本地环境中模拟Lambda服务器。

Enable AWS Lambda local

现在,如果我们运行目标,我们将在控制台中得到这样的东西。

2021-08-14T23:36:50-0300 info LocalLambdaServer : LocalLambdaServer started and listening on 127.0.0.1:7000, receiving events on /invoke
2021-08-14T23:36:50-0300 info Lambda : lambda lifecycle starting with Configuration
  General(logLevel: info))
  Lifecycle(id: 9908899204653, maxTimes: 0, stopSignal: TERM)
  RuntimeEngine(ip: 127.0.0.1, port: 7000, requestTimeout: nil

这意味着我们的Lambda函数运行在端口 http://localhost:700/invoke.因此,让我们继续前进,向该函数发出我们的第一个请求。

$ curl \
    --header "Content-Type: application/json" \
  --request POST \
  --data '{"name": "Bruno"}' \
  http://localhost:7000/invoke

如果我们得到类似这样的结果。 $ {"greeting":"Hello Bruno"},这意味着我们到目前为止做的都是正确的!。

我们有了第一个函数并运行,所以我们在一个相当好的位置继续前进并创建我们的HTTP API。

第四步:创建HTTP API

让我们从创建我们要处理的模型开始。由于我们将返回静态数据,我们也将创建一些辅助函数。继续,在Sources/ToDoList-API中创建一个新文件TodoItem.swift

struct ToDoItem: Codable {
    let id: Int
    let description: String
}

// MARK: - Static helpers

extension ToDoItem {
    static func getToDoList() -> [ToDoItem] {
        var list = [ToDoItem]()
        list.append(.init(id: 1, description: "Pay credit card"))
        list.append(.init(id: 2, description: "Clean apartment"))
        list.append(.init(id: 3, description: "Call John"))

        return list
    }

    static func getItem(with id: Int) -> ToDoItem? {
        return getToDoList().filter{ $0.id == id }.first
    }
}

我们需要做的下一件事是调整我们的Lambda函数以与APIGateway互动。为此,我们将使用AWSLambdaEvents中的两种类型作为我们函数的输入和输出。

  • APIGateway.V2.Request
  • APIGateway.V2.Response

在我们的main.swift文件中做如下修改。

typealias In = APIGateway.V2.Request
typealias Out = APIGateway.V2.Response

Lambda.run { (context, 
              request: In, 
              callback: @escaping (Result<Out, Error>) -> Void) in
    // Implementation... 
}

所以我们接收一个APIGateway.V2.Request类型作为输入,并且我们必须返回一个APIGateway.V2.Response类型作为输出。然而,我们想获得一个ToDoItem类型,当我们收到一个 POST时,我们想获得一个ToDoItem类型,而如果我们收到一个 GET.

这两种类型,APIGateway.V2.Request和**APIGateway.V2.Response,**都有一个body属性,我们将在其中接收和发送来自我们端点的有效载荷。这个属性是一个字符串类型,所以我们必须使用编码器(或者在我们想返回一些东西的情况下使用解码器)做一些转换,以便在发送回之前或开始处理之前使用我们的Swift类型。

我们的代码设计中唯一缺少的部分是我们如何区分不同的路径和方法。我们可以从我们的请求类型中访问端点路径。在我们的例子中,我们只打算有一个路径。 /todoitems.如果我们得到其他的路径,我们应该返回一个404错误。

让我们把所有的碎片放在一起,修改我们的lambda函数。

import Foundation
import AWSLambdaRuntime
import AWSLambdaEvents

typealias In = APIGateway.V2.Request
typealias Out = APIGateway.V2.Response

Lambda.run { (context,
              request: In,
              callback: @escaping (Result<Out, Error>) -> Void) in
    
    let routeKey = request.routeKey
    
    switch routeKey {
    
    case "GET /todoitems":
        let items = ToDoItem.getToDoList()
        let bodyOutput = try! JSONEncoder().encodeAsString(items)
        let output = Out(statusCode: .ok, headers: ["content-type": "application/json"], body: bodyOutput)
        callback(.success(output))
        
    case "GET /todoitems/{id}":
        if let idString = request.pathParameters?["id"], let id = Int(idString),
           let item = ToDoItem.getItem(with: id) {
            
            let bodyOutput = try! JSONEncoder().encodeAsString(item)
            let output = Out(statusCode: .ok, headers: ["content-type": "application/json"], body: bodyOutput)
            callback(.success(output))
        } else {
            callback(.success(Out(statusCode: .notFound)))
        }
        
    case "POST /todoitems":
        do {
            let input = try JSONDecoder().decode(ToDoItem.self, from: request.body ?? "")
            let bodyOutput = try JSONEncoder().encodeAsString(input)
            let output = Out(statusCode: .ok, headers: ["content-type": "application/json"], body: bodyOutput)
            callback(.success(output))
        } catch {
            callback(.success(Out(statusCode: .badRequest)))
        }
        
    default:
        callback(.success(Out(statusCode: .notFound)))
    }
}

// ---------------

extension JSONEncoder {
    func encodeAsString<T: Encodable>(_ value: T) throws -> String {
        try String(decoding: self.encode(value), as: Unicode.UTF8.self)
    }
}

extension JSONDecoder {
    func decode<T: Decodable>(_ type: T.Type, from string: String) throws -> T {
        try self.decode(type, from: Data(string.utf8))
    }
}

在本地测试API

现在我们已经有了一切,可以在将API部署到AWS之前开始测试它。让我们试着获取所有的项目。

$ curl \
    --header "Content-Type: application/json" \
  --request GET \
  http://localhost:7000/invoke/todoitems

我们会得到一个 404 - Not found错误,这很奇怪,因为我们已经在Lambda函数中以正确的方式配置了该端点。好吧,这是因为我们的本地Runtime只监听了以下的请求 http://localhost:7000/invoke.

此外,我们正在使用亚马逊API网关将我们的Lambda函数作为一个HTTP API暴露出来。这意味着所有传入的HTTP请求将被API Gateway转化为JSON数据,并向Lambda函数提供已经转化的有效载荷。然后,我们的函数将处理该JSON有效载荷,并以另一个JSON有效载荷本身进行响应,API网关将把它转化为HTTP响应。

因此,如果我们想模拟这种互动,我们必须提供一个HTTP请求(JSON格式),其中需要包括所有相关的信息,如我们想调用的方法、路由路径、正文等等。

这是一个标准的HTTP请求,在API网关进行转换之后。

{
    "routeKey":"GET /todoitems",
    "version":"2.0",
    "rawPath":"/todoitems",
    "requestContext":{
        "accountId":"",
        "apiId":"",
        "domainName":"",
        "domainPrefix":"",
        "stage": "",
        "requestId": "",
        "http":{
            "path":"/todoitems",
            "method":"GET",
            "protocol":"HTTP/1.1",
            "sourceIp":"",
            "userAgent":""
        },
        "time": "",
        "timeEpoch":0
    },
    "isBase64Encoded":false,
    "rawQueryString":"",
    "headers":{}
}

我们不需要提供所有的值,但所有的键必须存在。否则,我们会从Lambda函数中得到一个解码错误。

考虑到这一点,让我们再次提出一个请求。我们可以像以前一样使用终端或者像Postman这样的API客户端工具来做。

$ curl --header "Content-Type: application/json" \
  --request POST \
  --data '{
    "routeKey":"GET /todoitems",
    "version":"2.0",
    "rawPath":"/todoitems",
    "requestContext":{
        "accountId":"",
        "apiId":"",
        "domainName":"",
        "domainPrefix":"",
        "stage": "",
        "requestId": "",
        "http":{
            "path":"/todoitems",
            "method":"GET",
            "protocol":"HTTP/1.1",
            "sourceIp":"",
            "userAgent":""
        },
        "time": "",
        "timeEpoch":0
    },
    "isBase64Encoded":false,
    "rawQueryString":"",
    "headers":{}
}' \
http://localhost:7000/invoke

如果我们想只检索一个项目,我们需要在我们的数据JSON中添加一个条目。

"pathParameters": {"id": "1"}

最后,如果我们想测试这个 POST方法,我们必须在我们的数据JSON中添加以下条目。

"body": "{\"id\":1, \"description\": \"Test\"}"

而且,除此之外,我们还需要将routeKey修改为 GET /todoitems/{id}http.method属性为POST而不是GET

部署到AWS

编译和打包

我们将在亚马逊Linux 2操作系统上执行我们的Lambda函数,所以我们需要为这个特定的操作系统编译我们的函数。为方便起见,我们将使用Docker来做这件事。在你的根项目文件夹中创建一个名为Scripts的新文件夹。在这个文件夹中,创建一个新的build.sh文件,代码如下

docker run \
    --rm \
    --volume "$(pwd)/:/src" \
    --workdir "/src/" \
    swift:5.3.1-amazonlinux2 \
    swift build --product ToDoList-API -c release -Xswiftc -static-stdlib

理解Docker命令已经超出了本文的范围,但这段代码的作用是为容器编译我们的代码。如果你想了解更多关于使用Docker和它的可用命令,请查看官方文档

现在,继续在Scripts文件夹中创建另一个文件:package.sh

#!/bin/bash

set -eu

executable=$1

target=.build/lambda/$executable
rm -rf "$target"
mkdir -p "$target"
cp ".build/release/$executable" "$target/"
cd "$target"
ln -s "$executable" "bootstrap"
zip --symlinks lambda.zip *

这将创建一个具有正确结构的新压缩文件,供我们上传到AWS。

我们只需要按照这些简单的步骤来构建和打包我们的代码。

  1. $ sh ./Scripts/build.sh
  2. $ sh ./Scripts/package.sh ToDoList-API

在许多环境中,我们在执行这些脚本时可能会出现权限错误。如果发生这种情况,我们只需要通过运行以下命令将文件标记为可执行。

$ chmod +x Scripts/build.sh
$ chmod +x Scripts/package.sh

上传Lambda文件

下一步是创建我们的Lambda函数并上传我们刚刚生成的压缩文件。登录你的AWS账户,进入AWS Lambda,并点击创建函数

Crate AWS Lambda function - Step 1

在输入函数名称和运行时间选项后,点击创建函数按钮。你将被重定向到下一个屏幕,上传文件。

Crate AWS Lambda function - Step 2

点击.zip文件,在你的电脑上找到你的lambda.zip文件。它应该可以在 $ your-project-path/.build/lambda/ToDoList-API/lambda.zip

连接API网关

我们需要做的最后一件事是将我们的函数连接到API网关。从你的AWS控制台转到API网关仪表板,点击创建API。然后通过点击构建按钮选择HTTP API选项。

步骤1

  • 点击添加集成,选择Lambda选项
    • 搜索我们在上一节中刚刚创建的Lambda函数。
    • 确保版本是2.0
  • 为API选择一个名称。

API Gateway - Step 1

第2步

这里我们必须配置我们的路由。如果我们不想限制路由,我们可以在资源路径栏中使用$default。这将把所有的请求映射到我们的Lambda。

在本教程中,我们将设置我们在开始时定义的三个端点。

API Gateway - Step 2

第三步

在这一步,我们可以为我们的API配置不同的环境,如开发和生产。在我们的案例中,我们可以保留**$default**。

API Gateway - Step 3

第4步

查看所有的信息,然后点击创建

这就是了!现在我们的API已经部署到AWS。调用网址应该是这样的。 https://{your-gateway-id}.execute-api.us-east-1.amazonaws.com

测试它!

我们在这部分使用Postman,但你可以使用任何你想要的其他工具。

获取所有项目

Get all items reques

获取一个项目

Get one item request

创建新项目

Create new item request

使用Auth0保证API的安全

当然,你永远不会想留下一个未经认证的API--你将负责支付对它的每一次调用!。为了证明我们如何保护我们的端点,让我们使 GET /todoitems/{id}端点只能被认证的用户访问(在一个真正的应用中,我们会保护所有这些端点)。

为了实现这一目标,我们将用Auth0创建一个自定义的JSON Web Tokens(JWTs)授权器,并将其附加到我们的API网关端点。

创建一个新的Auth0 API

首先,登录你的Auth0账户,在左边的菜单中进入应用→API,然后点击+创建API按钮。

New Auth0 API

附加新的授权器

回到你的API网关仪表板,点击左侧面板上 "开发"部分下面的授权选项。

选择你想限制访问的端点;在我们的例子中,它将是 GET /todoitems/{id},然后点击创建并附加一个授权器

Authorizer

选择JWT类型并填写所需信息。

  • 名称:你想调用授权器的名称
  • 身份源$request.header.Authorization这意味着授权者可以访问授权头中的访问令牌。
  • 发行人的URLhttps://{tenant-name}.auth0.com.授权者使用它来验证JWT中的iss字段。你可以在应用程序→默认应用程序→域中找到你的Auth0租户名称。
  • 听众https://auth0-jwt-authorizer.这将被授权者用来验证JWT中的审计字段。这需要与我们之前配置的Auth0 API的标识符相匹配。

Add AWS Lambda authorizer

再次测试

如果我们现在尝试调用 GET /todoitems/{id},我们会得到一个未经授权的错误。

{
    "message": "Unauthorized"
}

这是因为如果我们想使用这个端点,我们必须提供一个认证头。在真正的应用中,在用户被我们的应用认证后,我们将返回一个有效的令牌,但只是为了测试,我们可以从Auth0获得一个令牌。

再次进入你的Auth0仪表板,在左边的面板上,点击应用→APIs→AWS JWT Authorizer 测试。找到响应部分,复制提供的承载令牌。

Auth0 dashboard

现在回到Postman,用这个令牌添加一个授权头,然后发送请求。

因为我们的请求有一个认证令牌,所以我们得到了一个响应!

Authentication Token Response

结论

有了这种类型的解决方案,作为iOS开发者,我们就可以开始转向移动全栈的角色,在这里我们不需要一个单独的团队来做后端。当然,每个团队和项目都是不同的。然而,对于小型项目或概念证明,这种解决方案应该是非常有效的。了解后端技术会增加你对软件的理解,使你成为任何iOS团队的更多资产。

Swift AWS Lambda Runtime有改进的余地(就像科技行业的一切),但它给了我们一个写后端代码的起点,而不需要学习新的开发语言。

如果你想要一个更详细的与数据库(DynamoDB)连接的例子,你可以在这里查看完整的项目。