Files
swift-aws-lambda-runtime/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift
T
Sébastien Stormacq e6ba07fd06 [example] Add example for Swift Service Lifecycle (#522)
Now that task cancellation works, re publishing this PR with a new
example for Swift Service Lifecycle
2025-07-30 06:40:55 +04:00

193 lines
6.8 KiB
Swift

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftAWSLambdaRuntime open source project
//
// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import AWSLambdaEvents
import AWSLambdaRuntime
import Logging
import PostgresNIO
import ServiceLifecycle
struct User: Codable {
let id: Int
let username: String
}
@main
struct LambdaFunction {
private let pgClient: PostgresClient
private let logger: Logger
private init() throws {
var logger = Logger(label: "ServiceLifecycleExample")
logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info
self.logger = logger
self.pgClient = try LambdaFunction.createPostgresClient(
host: Lambda.env("DB_HOST") ?? "localhost",
user: Lambda.env("DB_USER") ?? "postgres",
password: Lambda.env("DB_PASSWORD") ?? "secret",
dbName: Lambda.env("DB_NAME") ?? "servicelifecycle",
logger: self.logger
)
}
/// Function entry point when the runtime environment is created
private func main() async throws {
// Instantiate LambdaRuntime with a handler implementing the business logic of the Lambda function
let lambdaRuntime = LambdaRuntime(logger: self.logger, body: self.handler)
// Use a prelude service to execute PG code before setting up the Lambda service
// the PG code will run only once and will create the database schema and populate it with initial data
let preludeService = PreludeService(
service: lambdaRuntime,
prelude: {
try await prepareDatabase()
}
)
/// Use ServiceLifecycle to manage the initialization and termination
/// of the PGClient together with the LambdaRuntime
let serviceGroup = ServiceGroup(
services: [self.pgClient, preludeService],
gracefulShutdownSignals: [.sigterm],
cancellationSignals: [.sigint],
logger: self.logger
)
// launch the service groups
// this call will return upon termination or cancellation of all the services
try await serviceGroup.run()
// perform any cleanup here
}
/// Function handler. This code is called at each function invocation
/// input event is ignored in this demo.
private func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response {
var result: [User] = []
do {
// IMPORTANT - CURRENTLY, THIS CALL STOPS WHEN DB IS NOT REACHABLE
// See: https://github.com/vapor/postgres-nio/issues/489
// This is why there is a timeout, as suggested Fabian
// See: https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773
result = try await timeout(deadline: .seconds(3)) {
// query users
logger.trace("Querying database")
return try await self.queryUsers()
}
} catch {
logger.error("Database Error", metadata: ["cause": "\(String(reflecting: error))"])
}
return try .init(
statusCode: .ok,
headers: ["content-type": "application/json"],
encodableBody: result
)
}
/// Prepare the database
/// At first run, this functions checks the database exist and is populated.
/// This is useful for demo purposes. In real life, the database will contain data already.
private func prepareDatabase() async throws {
do {
// initial creation of the table. This will fails if it already exists
logger.trace("Testing if table exists")
try await self.pgClient.query(SQLStatements.createTable)
// it did not fail, it means the table is new and empty
logger.trace("Populate table")
try await self.pgClient.query(SQLStatements.populateTable)
} catch is PSQLError {
// when there is a database error, it means the table or values already existed
// ignore this error
logger.trace("Table exists already")
} catch {
// propagate other errors
throw error
}
}
/// Query the database
private func queryUsers() async throws -> [User] {
var users: [User] = []
let query = SQLStatements.queryAllUsers
let rows = try await self.pgClient.query(query)
for try await (id, username) in rows.decode((Int, String).self) {
self.logger.trace("\(id) : \(username)")
users.append(User(id: id, username: username))
}
return users
}
/// Create a postgres client
/// ...TODO
private static func createPostgresClient(
host: String,
user: String,
password: String,
dbName: String,
logger: Logger
) throws -> PostgresClient {
// Load the root certificate
let region = Lambda.env("AWS_REGION") ?? "us-east-1"
guard let pem = rootRDSCertificates[region] else {
logger.error("No root certificate found for the specified AWS region.")
throw LambdaErrors.missingRootCertificateForRegion(region)
}
let certificatePEM = Array(pem.utf8)
let rootCert = try NIOSSLCertificate.fromPEMBytes(certificatePEM)
// Add the root certificate to the TLS configuration
var tlsConfig = TLSConfiguration.makeClientConfiguration()
tlsConfig.trustRoots = .certificates(rootCert)
// Enable full verification
tlsConfig.certificateVerification = .fullVerification
let config = PostgresClient.Configuration(
host: host,
port: 5432,
username: user,
password: password,
database: dbName,
tls: .prefer(tlsConfig)
)
return PostgresClient(configuration: config)
}
private struct SQLStatements {
static let createTable: PostgresQuery =
"CREATE TABLE users (id SERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL);"
static let populateTable: PostgresQuery = "INSERT INTO users (username) VALUES ('alice'), ('bob'), ('charlie');"
static let queryAllUsers: PostgresQuery = "SELECT id, username FROM users"
}
static func main() async throws {
try await LambdaFunction().main()
}
}
public enum LambdaErrors: Error {
case missingRootCertificateForRegion(String)
}