使用无服务器应用程序模型 (SAM) 部署到 AWS Lambda

本指南说明了如何使用 AWS 无服务器应用程序模型 (SAM) 工具包在 AWS 上部署服务器端 Swift 工作负载。该工作负载是一个用于跟踪待办事项列表的 REST API。它使用 Amazon API Gateway 部署 API。API 方法使用 AWS Lambda 函数在 Amazon DynamoDB 数据库中存储和检索数据。

架构

Architecture

先决条件

要构建此示例应用程序,您需要

步骤 1:创建一个新的 SAM 项目

SAM 项目在您的 AWS 账户中创建资源(Lambda 函数、API Gateway 和 DynamoDB 表)。您在 YAML 模板中定义资源。

为您的项目创建一个文件夹,并新建一个 template.yml 文件。

mkdir swift-lambda-api && cd swift-lambda-api
touch template.yml

打开 template.yml 文件并添加以下代码。查看代码中的注释以确定每个部分创建的内容。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Resources:
  # DynamoDB table to store your data
  SwiftAPITable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      PrimaryKey:
        Name: id
        Type: String

  # Lambda function to put items to the database
  PutItemFunction:
    Type: AWS::Serverless::Function
    Properties:
      # package the function as a Docker image
      PackageType: Image
      Policies:
        # allow function to read and write to database table
        - DynamoDBCrudPolicy:
            TableName: !Ref SwiftAPITable
      Environment:
        # store database table name as an environment variable
        Variables:
          TABLE_NAME: !Ref SwiftAPITable
      Events:
        # handles the POST /item method of the REST API
        Api:
          Type: HttpApi
          Properties:
            Method: post
            Path: /item
    Metadata:
      # location of the code and Docker file for function
      DockerContext: ./src/put-item
      Dockerfile: Dockerfile
      DockerBuildArgs:
        TARGET_NAME: put-item

  # Lambda function to retrieve items from database
  GetItemsFunction:
    Type: AWS::Serverless::Function
    Properties:
      # package the function as a Docker image
      PackageType: Image
      Policies:
        # allow function to read and write to database table
        - DynamoDBCrudPolicy:
            TableName: !Ref SwiftAPITable
      Environment:
        # store database table name as an environment variable
        Variables:
          TABLE_NAME: !Ref SwiftAPITable
      Events:
        # handles the GET /items method of the REST API
        Api:
          Type: HttpApi
          Properties:
            Method: get
            Path: /items
    Metadata:
      # location of the code and Docker file for function
      DockerContext: ./src/get-items
      Dockerfile: Dockerfile
      DockerBuildArgs:
        TARGET_NAME: get-items

# 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"
  SwiftAPITable:
    Description: "DynamoDB Table Name"
    Value: !Ref SwiftAPITable

步骤 2:使用 SwiftPM 初始化 Lambda 函数

Lambda 函数用 Swift 编写,用于处理 API 事件。PutItem 函数处理 POST 请求以将项目添加到数据库。GetItems 函数处理 GET 请求以从数据库检索项目。

使用 Swift Package Manager 为每个函数初始化一个项目。您还需要向每个文件夹添加一个 Dockerfile

mkdir -p src/put-item
cd src/put-item
swift package init --type executable
touch Dockerfile

cd ../..

mkdir -p src/get-items
cd src/get-items
swift package init --type executable
touch Dockerfile

步骤 3:更新 Dockerfile

Docker 用于编译您的 Swift 代码并将镜像部署到 Lambda。将以下代码复制到您在每个函数的文件夹中创建的 Dockerfile 中。

# image used to compile your Swift code
FROM --platform=linux/amd64 public.ecr.aws/docker/library/swift:5.7.2-amazonlinux2 as builder

ARG TARGET_NAME

RUN yum -y install git jq tar zip openssl-devel
WORKDIR /build-lambda
RUN mkdir -p /Sources/$TARGET_NAME/
RUN mkdir -p /Tests/$TARGET_NAME/
ADD /Sources/ ./Sources/
ADD /Tests/ ./Tests/
COPY Package.swift .
RUN cd /build-lambda && swift package clean && swift build --static-swift-stdlib -c release

# image deplpoyed to AWS Lambda with your compiled executable
FROM public.ecr.aws/lambda/provided:al2-x86_64

ARG TARGET_NAME

RUN mkdir -p /var/task/
RUN mkdir -p /var/runtime/
COPY --from=builder /build-lambda/.build/release/$TARGET_NAME /var/task/lambdaExec
RUN chmod 755 /var/task/lambdaExec
RUN ln -s /var/task/lambdaExec /var/runtime/bootstrap
RUN chmod 755 /var/runtime/bootstrap
WORKDIR /var/task
CMD ["/var/task/lambdaExec"]

步骤 4:更新 Swift 依赖项

您的项目需要 3 个库。

您在 Package.swift 文件中定义这些库。将每个函数文件夹中的 Package.swift 文件的内容替换为以下代码。

src/put-item/Sources/put-item/Package.swift

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

import PackageDescription

let package = Package(
    name: "put-item",
    platforms: [.macOS(.v12)],
    dependencies: [
        .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime", branch: "main"),
        .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"),
        .package(url: "https://github.com/awslabs/aws-sdk-swift", from: "0.9.1")
    ],
    targets: [
        .executableTarget(
            name: "put-item",
            dependencies: [
                .product(name: "AWSLambdaRuntime",package: "swift-aws-lambda-runtime"),
                .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
                .product(name: "AWSDynamoDB", package: "aws-sdk-swift")
            ]),
        .testTarget(
            name: "put-itemTests",
            dependencies: ["put-item"]),
    ]
)

src/get-items/Sources/get-items/Package.swift

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

import PackageDescription

let package = Package(
    name: "get-items",
    platforms: [.macOS(.v12)],
    dependencies: [
        .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime", branch: "main"),
        .package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"),
        .package(url: "https://github.com/awslabs/aws-sdk-swift", from: "0.9.1")
    ],
    targets: [
        .executableTarget(
            name: "get-items",
            dependencies: [
                .product(name: "AWSLambdaRuntime",package: "swift-aws-lambda-runtime"),
                .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
                .product(name: "AWSDynamoDB", package: "aws-sdk-swift")
            ]),
        .testTarget(
            name: "get-itemsTests",
            dependencies: ["get-items"]),
    ]
)

步骤 5:更新 Lambda 函数源代码

将每个 Swift 项目的主代码文件的内容替换为以下代码。

src/put-item/Sources/put-item/put_item.swift

// import the packages required by our function
import Foundation
import AWSLambdaRuntime
import AWSLambdaEvents
import AWSDynamoDB

// define Codable struct for function response
struct Item : Codable {
    var id: String?
    let itemName: String
}

enum FunctionError: Error {
    case envError
}

@main
struct PutItemFunction: SimpleLambdaHandler {

    // Lambda Function handler
    func handle(_ event: APIGatewayV2Request, context: LambdaContext) async throws -> Item {

        print("event received:\(event)")

        // create a client to interact with DynamoDB
        let client = try await DynamoDBClient()

        // obtain DynamoDB table name from function's environment variables
        guard let tableName = ProcessInfo.processInfo.environment["TABLE_NAME"] else {
            throw FunctionError.envError
        }

        // decode data from APIGateway POST into a codable struct
        var item = try JSONDecoder().decode(
            Item.self,
            from: event.body!.data(using: .utf8)!
        )

        // generate a unique id for the key of the item
        item.id = UUID().uuidString

        // use SDK to put the item into the database and return the item with key value
        let input = PutItemInput(item: ["id": .s(item.id!), "itemName": .s(item.itemName)], tableName: tableName)

        _ = try await client.putItem(input: input)

        return item
    }
}

src/get-items/Sources/get_items/get_items.swift

// import the packages required by our function
import Foundation
import AWSLambdaRuntime
import AWSLambdaEvents
import AWSDynamoDB

// define Codable struct for function response
struct Item : Codable {
    var id: String = ""
    var itemName: String = ""
}

enum FunctionError: Error {
    case envError
}

@main
struct GetItemsFunction: SimpleLambdaHandler {

    // Lambda Function handler
    func handle(_ event: APIGatewayV2Request, context: LambdaContext) async throws -> [Item] {

        print("event received:\(event)")

        // create a client to interact with DynamoDB
        let client = try await DynamoDBClient()

        // obtain DynamoDB table name from function's environment variables
        guard let tableName = ProcessInfo.processInfo.environment["TABLE_NAME"] else {
            throw FunctionError.envError
        }

        // use SDK to retrieve items from table
        let input = ScanInput(tableName: tableName)
        let response = try await client.scan(input: input)

        // return items in an array
        return response.items!.map() {i in
            var item = Item()

            if case .s(let value) = i["id"] {
                item.id = value
            }

            if case .s(let value) = i["itemName"] {
                item.itemName = value
            }

            return item
        }
    }
}

步骤 6:构建 SAM 项目

构建您的 SAM 项目使用您机器上的 Docker 将您的 Swift 代码编译成 Docker 镜像。从您的项目根文件夹 (swift-lambda-api) 运行以下命令。

sam build

步骤 7:部署 SAM 项目

部署您的 SAM 项目会在您的 AWS 账户中创建 Lambda 函数、API Gateway 和 DynamoDB 数据库。

sam deploy --guided

接受对每个提示的默认响应,以下两个提示除外

PutItemFunction may not have authorization defined, Is this okay? [y/N]: y
GetItemsFunction may not have authorization defined, Is this okay? [y/N]: y

该项目创建了一个公开访问的 API 端点。这些警告是为了通知您 API 没有授权。如果您有兴趣向 API 添加授权,请参阅 SAM 文档

步骤 8:使用您的 API

在部署结束时,SAM 会显示您的 API Gateway 的端点

Outputs
----------------------------------------------------------------------------------------
Key                 SwiftAPIEndpoint
Description         API Gateway endpoint URL for your application
Value               https://[your-api-id].execute-api.[your-aws-region].amazonaws.com
----------------------------------------------------------------------------------------

使用 cURL 或 Postman 等工具与您的 API 交互。将 [your-api-endpoint] 替换为部署输出中的 SwiftAPIEndpoint 值。

添加一个待办事项列表项

curl --request POST 'https://[your-api-endpoint]/item' --header 'Content-Type: application/json' --data-raw '{"itemName": "my todo item"}'

检索待办事项列表项

curl https://[your-api-endpoint]/items

清理

当完成您的应用程序后,使用 SAM 将其从您的 AWS 账户中删除。对所有提示回答 是 (y)

sam delete