From c4c71ccc3b7eb079e90b21848a894c44423d3d4b Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 5 Jan 2025 23:21:42 +0100 Subject: [PATCH 1/6] initial commit --- Package.swift | 2 ++ Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index d2c92fdc..9460f98f 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,7 @@ let package = Package( dependencies: [ .package(url: "https://github.com/apple/swift-nio.git", from: "2.76.0"), .package(url: "https://github.com/apple/swift-log.git", from: "1.5.4"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), ], targets: [ .target( @@ -36,6 +37,7 @@ let package = Package( .product(name: "NIOCore", package: "swift-nio"), .product(name: "NIOConcurrencyHelpers", package: "swift-nio"), .product(name: "NIOPosix", package: "swift-nio"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), ], swiftSettings: [.swiftLanguageMode(.v5)] ), diff --git a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift index 317ee7ea..6f0187e7 100644 --- a/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntimeCore/LambdaRuntime.swift @@ -15,6 +15,7 @@ import Logging import NIOConcurrencyHelpers import NIOCore +import ServiceLifecycle #if canImport(FoundationEssentials) import FoundationEssentials @@ -25,7 +26,7 @@ import Foundation // We need `@unchecked` Sendable here, as `NIOLockedValueBox` does not understand `sending` today. // We don't want to use `NIOLockedValueBox` here anyway. We would love to use Mutex here, but this // sadly crashes the compiler today. -public final class LambdaRuntime: @unchecked Sendable where Handler: StreamingLambdaHandler { +public final class LambdaRuntime: Service, @unchecked Sendable where Handler: StreamingLambdaHandler { // TODO: We want to change this to Mutex as soon as this doesn't crash the Swift compiler on Linux anymore let handlerMutex: NIOLockedValueBox let logger: Logger From 54d2fd10945449e53645d62e5b3b301d322cde52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 30 Jun 2025 09:07:15 +0200 Subject: [PATCH 2/6] merge with recent changes --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 3553be16..74a0d508 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -26,7 +26,7 @@ import Foundation // We need `@unchecked` Sendable here, as `NIOLockedValueBox` does not understand `sending` today. // We don't want to use `NIOLockedValueBox` here anyway. We would love to use Mutex here, but this // sadly crashes the compiler today. -public final class LambdaRuntime: Service, @unchecked Sendable where Handler: StreamingLambdaHandler { +public final class LambdaRuntime: @unchecked Sendable where Handler: StreamingLambdaHandler { // TODO: We want to change this to Mutex as soon as this doesn't crash the Swift compiler on Linux anymore @usableFromInline let handlerMutex: NIOLockedValueBox From 5c4d33b9780460f7caa5efa00076f8f0469af41c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 30 Jun 2025 09:10:26 +0200 Subject: [PATCH 3/6] remove unneeded import --- Sources/AWSLambdaRuntime/LambdaRuntime.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/LambdaRuntime.swift b/Sources/AWSLambdaRuntime/LambdaRuntime.swift index 74a0d508..7aba2812 100644 --- a/Sources/AWSLambdaRuntime/LambdaRuntime.swift +++ b/Sources/AWSLambdaRuntime/LambdaRuntime.swift @@ -15,7 +15,6 @@ import Logging import NIOConcurrencyHelpers import NIOCore -import ServiceLifecycle #if canImport(FoundationEssentials) import FoundationEssentials From 0993cad7f89a9e45be834859af8bac5592e1c6c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 30 Jun 2025 09:19:54 +0200 Subject: [PATCH 4/6] add example code --- Examples/ServiceLifecycle/.gitignore | 8 ++ Examples/ServiceLifecycle/Package.swift | 56 +++++++++++ .../ServiceLifecycle/Sources/Lambda.swift | 92 ++++++++++++++++++ .../Sources/RootRDSCert.swift | 95 +++++++++++++++++++ 4 files changed, 251 insertions(+) create mode 100644 Examples/ServiceLifecycle/.gitignore create mode 100644 Examples/ServiceLifecycle/Package.swift create mode 100644 Examples/ServiceLifecycle/Sources/Lambda.swift create mode 100644 Examples/ServiceLifecycle/Sources/RootRDSCert.swift diff --git a/Examples/ServiceLifecycle/.gitignore b/Examples/ServiceLifecycle/.gitignore new file mode 100644 index 00000000..85146e6e --- /dev/null +++ b/Examples/ServiceLifecycle/.gitignore @@ -0,0 +1,8 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc \ No newline at end of file diff --git a/Examples/ServiceLifecycle/Package.swift b/Examples/ServiceLifecycle/Package.swift new file mode 100644 index 00000000..a5c2fa59 --- /dev/null +++ b/Examples/ServiceLifecycle/Package.swift @@ -0,0 +1,56 @@ +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +// needed for CI to test the local version of the library +import struct Foundation.URL + +let package = Package( + name: "LambdaWithServiceLifecycle", + platforms: [ + .macOS(.v15) + ], + dependencies: [ + .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.23.0"), + .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), + .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), + ], + targets: [ + .executableTarget( + name: "LambdaWithServiceLifecycle", + dependencies: [ + .product(name: "PostgresNIO", package: "postgres-nio"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + ] + ) + ] +) + +if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], + localDepsPath != "", + let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]), + v.isDirectory == true +{ + // when we use the local runtime as deps, let's remove the dependency added above + let indexToRemove = package.dependencies.firstIndex { dependency in + if case .sourceControl( + name: _, + location: "https://github.com/swift-server/swift-aws-lambda-runtime.git", + requirement: _ + ) = dependency.kind { + return true + } + return false + } + if let indexToRemove { + package.dependencies.remove(at: indexToRemove) + } + + // then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..) + print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)") + package.dependencies += [ + .package(name: "swift-aws-lambda-runtime", path: localDepsPath) + ] +} \ No newline at end of file diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift new file mode 100644 index 00000000..64ec7b1a --- /dev/null +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 AWSLambdaRuntime +import Logging +import PostgresNIO +import ServiceLifecycle + +@main +struct LambdaFunction { + + static func main() async throws { + + var logger = Logger(label: "ServiceLifecycleExample") + logger.logLevel = .trace + + let pgClient = try preparePostgresClient( + host: Lambda.env("DB_HOST") ?? "localhost", + user: Lambda.env("DB_USER") ?? "postgres", + password: Lambda.env("DB_PASSWORD") ?? "secret", + dbName: Lambda.env("DB_NAME") ?? "test" + ) + + /// Instantiate LambdaRuntime with a closure handler implementing the business logic of the Lambda function + let runtime = LambdaRuntimeService(logger: logger) { (event: String, context: LambdaContext) in + + do { + // Use initialized service within the handler + // IMPORTANT - CURRENTLY WHEN THERE IS AN ERROR, THIS CALL HANGS WHEN DB IS NOT REACHABLE + // https://github.com/vapor/postgres-nio/issues/489 + let rows = try await pgClient.query("SELECT id, username FROM users") + for try await (id, username) in rows.decode((Int, String).self) { + logger.debug("\(id) : \(username)") + } + } catch { + logger.error("PG Error: \(error)") + } + } + + /// Use ServiceLifecycle to manage the initialization and termination + /// of the PGClient together with the LambdaRuntime + let serviceGroup = ServiceGroup( + services: [pgClient, runtime], + gracefulShutdownSignals: [.sigterm, .sigint], // add SIGINT for CTRL+C in local testing + // cancellationSignals: [.sigint], + logger: logger + ) + try await serviceGroup.run() + + // perform any cleanup here + } + + private static func preparePostgresClient( + host: String, + user: String, + password: String, + dbName: String + ) throws -> PostgresClient { + + var tlsConfig = TLSConfiguration.makeClientConfiguration() + // Load the root certificate + let rootCert = try NIOSSLCertificate.fromPEMBytes(Array(eu_central_1_bundle_pem.utf8)) + + // Add the root certificate to the TLS configuration + 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) + } +} \ No newline at end of file diff --git a/Examples/ServiceLifecycle/Sources/RootRDSCert.swift b/Examples/ServiceLifecycle/Sources/RootRDSCert.swift new file mode 100644 index 00000000..788959a3 --- /dev/null +++ b/Examples/ServiceLifecycle/Sources/RootRDSCert.swift @@ -0,0 +1,95 @@ +/===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// you can download the root certificate for your RDS instance region from the following link: +// https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html + +let eu_central_1_bundle_pem = """ + -----BEGIN CERTIFICATE----- + MIICtDCCAjmgAwIBAgIQenQbcP/Zbj9JxvZ+jXbRnTAKBggqhkjOPQQDAzCBmTEL + MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x + EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTIwMAYDVQQDDClBbWF6 + b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIEVDQzM4NCBHMTEQMA4GA1UEBwwH + U2VhdHRsZTAgFw0yMTA1MjEyMjMzMjRaGA8yMTIxMDUyMTIzMzMyNFowgZkxCzAJ + BgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMuMRMw + EQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEyMDAGA1UEAwwpQW1hem9u + IFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl + YXR0bGUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATlBHiEM9LoEb1Hdnd5j2VpCDOU + 5nGuFoBD8ROUCkFLFh5mHrHfPXwBc63heW9WrP3qnDEm+UZEUvW7ROvtWCTPZdLz + Z4XaqgAlSqeE2VfUyZOZzBSgUUJk7OlznXfkCMOjQjBAMA8GA1UdEwEB/wQFMAMB + Af8wHQYDVR0OBBYEFDT/ThjQZl42Nv/4Z/7JYaPNMly2MA4GA1UdDwEB/wQEAwIB + hjAKBggqhkjOPQQDAwNpADBmAjEAnZWmSgpEbmq+oiCa13l5aGmxSlfp9h12Orvw + Dq/W5cENJz891QD0ufOsic5oGq1JAjEAp5kSJj0MxJBTHQze1Aa9gG4sjHBxXn98 + 4MP1VGsQuhfndNHQb4V0Au7OWnOeiobq + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIIEBTCCAu2gAwIBAgIRAO8bekN7rUReuNPG8pSTKtEwDQYJKoZIhvcNAQELBQAw + gZoxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ + bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEzMDEGA1UEAwwq + QW1hem9uIFJEUyBldS1jZW50cmFsLTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYD + VQQHDAdTZWF0dGxlMCAXDTIxMDUyMTIyMjM0N1oYDzIwNjEwNTIxMjMyMzQ3WjCB + mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu + Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB + bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNV + BAcMB1NlYXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCTTYds + Tray+Q9VA5j5jTh5TunHKFQzn68ZbOzdqaoi/Rq4ohfC0xdLrxCpfqn2TGDHN6Zi + 2qGK1tWJZEd1H0trhzd9d1CtGK+3cjabUmz/TjSW/qBar7e9MA67/iJ74Gc+Ww43 + A0xPNIWcL4aLrHaLm7sHgAO2UCKsrBUpxErOAACERScVYwPAfu79xeFcX7DmcX+e + lIqY16pQAvK2RIzrekSYfLFxwFq2hnlgKHaVgZ3keKP+nmXcXmRSHQYUUr72oYNZ + HcNYl2+gxCc9ccPEHM7xncVEKmb5cWEWvVoaysgQ+osi5f5aQdzgC2X2g2daKbyA + XL/z5FM9GHpS5BJjAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE + FBDAiJ7Py9/A9etNa/ebOnx5l5MGMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0B + AQsFAAOCAQEALMh/+81fFPdJV/RrJUeoUvFCGMp8iaANu97NpeJyKitNOv7RoeVP + WjivS0KcCqZaDBs+p6IZ0sLI5ZH098LDzzytcfZg0PsGqUAb8a0MiU/LfgDCI9Ee + jsOiwaFB8k0tfUJK32NPcIoQYApTMT2e26lPzYORSkfuntme2PTHUnuC7ikiQrZk + P+SZjWgRuMcp09JfRXyAYWIuix4Gy0eZ4rpRuaTK6mjAb1/LYoNK/iZ/gTeIqrNt + l70OWRsWW8jEmSyNTIubGK/gGGyfuZGSyqoRX6OKHESkP6SSulbIZHyJ5VZkgtXo + 2XvyRyJ7w5pFyoofrL3Wv0UF8yt/GDszmg== + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + 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 + 89cEIGdbjsA= + -----END CERTIFICATE----- + """ \ No newline at end of file From 8740d70a74d5d46d814057a56eefadc9712ae101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 30 Jun 2025 10:24:09 +0200 Subject: [PATCH 5/6] update example --- Examples/ServiceLifecycle/Package.swift | 4 +- .../ServiceLifecycle/Sources/Lambda.swift | 45 +++++++++++-------- .../Sources/RootRDSCert.swift | 4 +- 3 files changed, 31 insertions(+), 22 deletions(-) diff --git a/Examples/ServiceLifecycle/Package.swift b/Examples/ServiceLifecycle/Package.swift index a5c2fa59..0c3ee2b9 100644 --- a/Examples/ServiceLifecycle/Package.swift +++ b/Examples/ServiceLifecycle/Package.swift @@ -12,7 +12,7 @@ let package = Package( .macOS(.v15) ], dependencies: [ - .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.23.0"), + .package(url: "https://github.com/vapor/postgres-nio.git", from: "1.26.0"), .package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), ], @@ -53,4 +53,4 @@ if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"], package.dependencies += [ .package(name: "swift-aws-lambda-runtime", path: localDepsPath) ] -} \ No newline at end of file +} diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index 64ec7b1a..f3be55b7 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -21,32 +21,26 @@ import ServiceLifecycle struct LambdaFunction { static func main() async throws { + LambdaFunction().main() + } - var logger = Logger(label: "ServiceLifecycleExample") + private let pgClient: PostgresClient + private var logger: Logger + private init() throws { + self.logger = Logger(label: "ServiceLifecycleExample") logger.logLevel = .trace - let pgClient = try preparePostgresClient( + self.pgClient = try preparePostgresClient( host: Lambda.env("DB_HOST") ?? "localhost", user: Lambda.env("DB_USER") ?? "postgres", password: Lambda.env("DB_PASSWORD") ?? "secret", dbName: Lambda.env("DB_NAME") ?? "test" ) + } + private func main() { /// Instantiate LambdaRuntime with a closure handler implementing the business logic of the Lambda function - let runtime = LambdaRuntimeService(logger: logger) { (event: String, context: LambdaContext) in - - do { - // Use initialized service within the handler - // IMPORTANT - CURRENTLY WHEN THERE IS AN ERROR, THIS CALL HANGS WHEN DB IS NOT REACHABLE - // https://github.com/vapor/postgres-nio/issues/489 - let rows = try await pgClient.query("SELECT id, username FROM users") - for try await (id, username) in rows.decode((Int, String).self) { - logger.debug("\(id) : \(username)") - } - } catch { - logger.error("PG Error: \(error)") - } - } + let runtime = LambdaRuntime(logger: logger, body: handler) /// Use ServiceLifecycle to manage the initialization and termination /// of the PGClient together with the LambdaRuntime @@ -59,9 +53,24 @@ struct LambdaFunction { try await serviceGroup.run() // perform any cleanup here + + } + + private func handler(event: String, context: LambdaContext) -> String { + do { + // Use initialized service within the handler + // IMPORTANT - CURRENTLY WHEN THERE IS AN ERROR, THIS CALL HANGS WHEN DB IS NOT REACHABLE + // https://github.com/vapor/postgres-nio/issues/489 + let rows = try await pgClient.query("SELECT id, username FROM users") + for try await (id, username) in rows.decode((Int, String).self) { + logger.debug("\(id) : \(username)") + } + } catch { + logger.error("PG Error: \(error)") + } } - private static func preparePostgresClient( + private func preparePostgresClient( host: String, user: String, password: String, @@ -89,4 +98,4 @@ struct LambdaFunction { return PostgresClient(configuration: config) } -} \ No newline at end of file +} diff --git a/Examples/ServiceLifecycle/Sources/RootRDSCert.swift b/Examples/ServiceLifecycle/Sources/RootRDSCert.swift index 788959a3..23cab9f3 100644 --- a/Examples/ServiceLifecycle/Sources/RootRDSCert.swift +++ b/Examples/ServiceLifecycle/Sources/RootRDSCert.swift @@ -1,4 +1,4 @@ -/===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// // // This source file is part of the SwiftAWSLambdaRuntime open source project // @@ -92,4 +92,4 @@ let eu_central_1_bundle_pem = """ 4vll0F/xgVRHTgDVQ8b8sxdhSYlqB4Wc2Ym41YRz+X2yPqk3typEZBpc4P5Tt1/N 89cEIGdbjsA= -----END CERTIFICATE----- - """ \ No newline at end of file + """ From a88de4765ffbccb8df7d549b340a12e080409a6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 30 Jun 2025 12:05:08 +0200 Subject: [PATCH 6/6] add sam template --- Examples/ServiceLifecycle/.gitignore | 3 +- Examples/ServiceLifecycle/README.md | 210 ++++++++++++++ .../ServiceLifecycle/Sources/Lambda.swift | 22 +- Examples/ServiceLifecycle/deploy.sh | 22 ++ Examples/ServiceLifecycle/samconfig.toml | 29 ++ Examples/ServiceLifecycle/template.yaml | 267 ++++++++++++++++++ 6 files changed, 542 insertions(+), 11 deletions(-) create mode 100644 Examples/ServiceLifecycle/README.md create mode 100755 Examples/ServiceLifecycle/deploy.sh create mode 100644 Examples/ServiceLifecycle/samconfig.toml create mode 100644 Examples/ServiceLifecycle/template.yaml diff --git a/Examples/ServiceLifecycle/.gitignore b/Examples/ServiceLifecycle/.gitignore index 85146e6e..c35fd53d 100644 --- a/Examples/ServiceLifecycle/.gitignore +++ b/Examples/ServiceLifecycle/.gitignore @@ -5,4 +5,5 @@ xcuserdata/ DerivedData/ .swiftpm/configuration/registries.json .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata -.netrc \ No newline at end of file +.netrc +.amazonq \ No newline at end of file diff --git a/Examples/ServiceLifecycle/README.md b/Examples/ServiceLifecycle/README.md new file mode 100644 index 00000000..837307be --- /dev/null +++ b/Examples/ServiceLifecycle/README.md @@ -0,0 +1,210 @@ +# ServiceLifecycle Lambda with PostgreSQL + +This example demonstrates a Swift Lambda function that uses ServiceLifecycle to manage a PostgreSQL connection. The function connects to a publicly accessible RDS PostgreSQL database and queries user data. + +## Architecture + +- **Swift Lambda Function**: Uses ServiceLifecycle to manage PostgreSQL client lifecycle +- **PostgreSQL RDS**: Publicly accessible database instance +- **API Gateway**: HTTP endpoint to invoke the Lambda function +- **VPC**: Custom VPC with public subnets for RDS and Lambda + +## Prerequisites + +- Swift 6.x toolchain +- Docker (for building Lambda functions) +- AWS CLI configured with appropriate permissions +- SAM CLI installed + +## Database Schema + +The Lambda function expects a `users` table with the following structure: + +```sql +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL +); + +-- Insert some sample data +INSERT INTO users (username) VALUES ('alice'), ('bob'), ('charlie'); +``` + +## Deployment + +### Option 1: Using the deployment script + +```bash +./deploy.sh +``` + +### Option 2: Manual deployment + +1. **Build the Lambda function:** + ```bash + swift package archive --allow-network-connections docker + ``` + +2. **Deploy with SAM:** + ```bash + sam deploy + ``` + +### Option 3: Deploy with custom parameters + +```bash +sam deploy --parameter-overrides \ + DBUsername=myuser \ + DBPassword=MySecurePassword123! \ + DBName=mydatabase +``` + +## Getting Connection Details + +After deployment, get the database connection details: + +```bash +aws cloudformation describe-stacks \ + --stack-name servicelifecycle-stack \ + --query 'Stacks[0].Outputs' +``` + +The output will include: +- **DatabaseEndpoint**: Hostname to connect to +- **DatabasePort**: Port number (5432) +- **DatabaseName**: Database name +- **DatabaseUsername**: Username +- **DatabasePassword**: Password +- **DatabaseConnectionString**: Complete connection string + +## Connecting to the Database + +### Using psql + +```bash +# Get the connection details from CloudFormation outputs +DB_HOST=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseEndpoint`].OutputValue' --output text) +DB_USER=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseUsername`].OutputValue' --output text) +DB_NAME=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseName`].OutputValue' --output text) +DB_PASSWORD=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabasePassword`].OutputValue' --output text) + +# Connect with psql +psql -h $DB_HOST -U $DB_USER -d $DB_NAME +``` + +### Using connection string + +```bash +# Get the complete connection string +CONNECTION_STRING=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseConnectionString`].OutputValue' --output text) + +# Connect with psql +psql "$CONNECTION_STRING" +``` + +## Setting up the Database + +Once connected to the database, create the required table and sample data: + +```sql +-- Create the users table +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL +); + +-- Insert sample data +INSERT INTO users (username) VALUES + ('alice'), + ('bob'), + ('charlie'), + ('diana'), + ('eve'); +``` + +## Testing the Lambda Function + +Get the API Gateway endpoint and test the function: + +```bash +# Get the API endpoint +API_ENDPOINT=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text) + +# Test the function +curl "$API_ENDPOINT" +``` + +The function will: +1. Connect to the PostgreSQL database +2. Query the `users` table +3. Log the results +4. Return "Done" + +## Monitoring + +Check the Lambda function logs: + +```bash +sam logs -n ServiceLifecycleLambda --stack-name servicelifecycle-stack --tail +``` + +## Security Considerations + +โš ๏ธ **Important**: This example creates a publicly accessible PostgreSQL database for demonstration purposes. In production: + +1. **Use private subnets** and VPC endpoints +2. **Implement proper authentication** (IAM database authentication) +3. **Use AWS Secrets Manager** for password management +4. **Enable encryption** at rest and in transit +5. **Configure proper security groups** with minimal required access +6. **Enable database logging** and monitoring + +## Cost Optimization + +The template uses: +- `db.t3.micro` instance (eligible for free tier) +- Minimal storage allocation (20GB) +- No Multi-AZ deployment +- No automated backups + +For production workloads, adjust these settings based on your requirements. + +## Cleanup + +To delete all resources: + +```bash +sam delete --stack-name servicelifecycle-stack +``` + +## Troubleshooting + +### Lambda can't connect to database + +1. Check security groups allow traffic on port 5432 +2. Verify the database is publicly accessible +3. Check VPC configuration and routing +4. Verify database credentials in environment variables + +### Database connection timeout + +The PostgreSQL client may hang if the database is unreachable. This is a known issue with PostgresNIO. Ensure: +1. Database is running and accessible +2. Security groups are properly configured +3. Network connectivity is available + +### Build failures + +Ensure you have: +1. Swift 6.x toolchain installed +2. Docker running +3. Proper network connectivity for downloading dependencies + +## Files + +- `template.yaml`: SAM template defining all AWS resources +- `samconfig.toml`: SAM configuration file +- `deploy.sh`: Deployment script +- `Sources/Lambda.swift`: Swift Lambda function code +- `Sources/RootRDSCert.swift`: RDS root certificate for SSL connections +- `Package.swift`: Swift package definition diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index f3be55b7..7bb54cf6 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -21,33 +21,34 @@ import ServiceLifecycle struct LambdaFunction { static func main() async throws { - LambdaFunction().main() + try await LambdaFunction().main() } private let pgClient: PostgresClient private var logger: Logger private init() throws { self.logger = Logger(label: "ServiceLifecycleExample") - logger.logLevel = .trace - self.pgClient = try preparePostgresClient( + self.pgClient = try LambdaFunction.preparePostgresClient( host: Lambda.env("DB_HOST") ?? "localhost", user: Lambda.env("DB_USER") ?? "postgres", password: Lambda.env("DB_PASSWORD") ?? "secret", dbName: Lambda.env("DB_NAME") ?? "test" ) } - private func main() { + private func main() async throws { - /// Instantiate LambdaRuntime with a closure handler implementing the business logic of the Lambda function - let runtime = LambdaRuntime(logger: logger, body: handler) + // Instantiate LambdaRuntime with a handler implementing the business logic of the Lambda function + // ok when https://github.com/swift-server/swift-aws-lambda-runtime/pull/523 will be merged + //let runtime = LambdaRuntime(logger: logger, body: handler) + let runtime = LambdaRuntime(body: handler) /// Use ServiceLifecycle to manage the initialization and termination /// of the PGClient together with the LambdaRuntime let serviceGroup = ServiceGroup( services: [pgClient, runtime], - gracefulShutdownSignals: [.sigterm, .sigint], // add SIGINT for CTRL+C in local testing - // cancellationSignals: [.sigint], + // gracefulShutdownSignals: [.sigterm, .sigint], // add SIGINT for CTRL+C in local testing + cancellationSignals: [.sigint], logger: logger ) try await serviceGroup.run() @@ -56,7 +57,7 @@ struct LambdaFunction { } - private func handler(event: String, context: LambdaContext) -> String { + private func handler(event: String, context: LambdaContext) async -> String { do { // Use initialized service within the handler // IMPORTANT - CURRENTLY WHEN THERE IS AN ERROR, THIS CALL HANGS WHEN DB IS NOT REACHABLE @@ -68,9 +69,10 @@ struct LambdaFunction { } catch { logger.error("PG Error: \(error)") } + return "Done" } - private func preparePostgresClient( + private static func preparePostgresClient( host: String, user: String, password: String, diff --git a/Examples/ServiceLifecycle/deploy.sh b/Examples/ServiceLifecycle/deploy.sh new file mode 100755 index 00000000..8f41e02e --- /dev/null +++ b/Examples/ServiceLifecycle/deploy.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +# ServiceLifecycle Lambda Deployment Script +set -e + +echo "๐Ÿš€ Building and deploying ServiceLifecycle Lambda with PostgreSQL..." + +# Build the Lambda function +echo "๐Ÿ“ฆ Building Swift Lambda function..." +swift package --disable-sandbox archive --allow-network-connections docker + +# Deploy with SAM +echo "๐ŸŒฉ๏ธ Deploying with SAM..." +sam deploy + +echo "โœ… Deployment complete!" +echo "" +echo "๐Ÿ“‹ To get the database connection details, run:" +echo "aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs'" +echo "" +echo "๐Ÿงช To test the Lambda function:" +echo "curl \$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==\`APIGatewayEndpoint\`].OutputValue' --output text)" diff --git a/Examples/ServiceLifecycle/samconfig.toml b/Examples/ServiceLifecycle/samconfig.toml new file mode 100644 index 00000000..4171fc12 --- /dev/null +++ b/Examples/ServiceLifecycle/samconfig.toml @@ -0,0 +1,29 @@ +# SAM configuration file for ServiceLifecycle example +version = 0.1 + +[default.global.parameters] +stack_name = "servicelifecycle-stack" + +[default.build.parameters] +cached = true +parallel = true + +[default.deploy.parameters] +capabilities = "CAPABILITY_IAM" +confirm_changeset = true +resolve_s3 = true +s3_prefix = "servicelifecycle" +region = "us-east-1" +image_repositories = [] + +[default.package.parameters] +resolve_s3 = true + +[default.sync.parameters] +watch = true + +[default.local_start_api.parameters] +warm_containers = "EAGER" + +[default.local_start_lambda.parameters] +warm_containers = "EAGER" diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle/template.yaml new file mode 100644 index 00000000..b25be3b0 --- /dev/null +++ b/Examples/ServiceLifecycle/template.yaml @@ -0,0 +1,267 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for ServiceLifecycle Lambda with PostgreSQL RDS + +Parameters: + DBUsername: + Type: String + Default: postgres + Description: Database username + MinLength: 1 + MaxLength: 16 + AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' + ConstraintDescription: Must begin with a letter and contain only alphanumeric characters + + DBPassword: + Type: String + Default: MySecretPassword123! + Description: Database password + MinLength: 8 + MaxLength: 41 + NoEcho: true + ConstraintDescription: Must be at least 8 characters long + + DBName: + Type: String + Default: servicelifecycle + Description: Database name + MinLength: 1 + MaxLength: 64 + AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' + ConstraintDescription: Must begin with a letter and contain only alphanumeric characters + +Resources: + # VPC for RDS (required for public access configuration) + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: ServiceLifecycle-VPC + + # Internet Gateway for public access + InternetGateway: + Type: AWS::EC2::InternetGateway + Properties: + Tags: + - Key: Name + Value: ServiceLifecycle-IGW + + # Attach Internet Gateway to VPC + InternetGatewayAttachment: + Type: AWS::EC2::VPCGatewayAttachment + Properties: + InternetGatewayId: !Ref InternetGateway + VpcId: !Ref VPC + + # Public Subnet 1 + PublicSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [0, !GetAZs ''] + CidrBlock: 10.0.1.0/24 + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: ServiceLifecycle-Public-Subnet-1 + + # Public Subnet 2 (required for RDS subnet group) + PublicSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [1, !GetAZs ''] + CidrBlock: 10.0.2.0/24 + MapPublicIpOnLaunch: true + Tags: + - Key: Name + Value: ServiceLifecycle-Public-Subnet-2 + + # Route Table for public subnets + PublicRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: ServiceLifecycle-Public-Routes + + # Route to Internet Gateway + DefaultPublicRoute: + Type: AWS::EC2::Route + DependsOn: InternetGatewayAttachment + Properties: + RouteTableId: !Ref PublicRouteTable + DestinationCidrBlock: 0.0.0.0/0 + GatewayId: !Ref InternetGateway + + # Associate public subnets with route table + PublicSubnet1RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet1 + + PublicSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PublicRouteTable + SubnetId: !Ref PublicSubnet2 + + # Security Group for RDS (allows public access) + DatabaseSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: ServiceLifecycle-DB-SG + GroupDescription: Security group for PostgreSQL database - allows public access + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + CidrIp: 0.0.0.0/0 + Description: Allow PostgreSQL access from anywhere + Tags: + - Key: Name + Value: ServiceLifecycle-DB-SecurityGroup + + # Security Group for Lambda + LambdaSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: ServiceLifecycle-Lambda-SG + GroupDescription: Security group for Lambda function + VpcId: !Ref VPC + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Description: Allow all outbound traffic + Tags: + - Key: Name + Value: ServiceLifecycle-Lambda-SecurityGroup + + # DB Subnet Group (required for RDS) + DatabaseSubnetGroup: + Type: AWS::RDS::DBSubnetGroup + Properties: + DBSubnetGroupDescription: Subnet group for PostgreSQL database + SubnetIds: + - !Ref PublicSubnet1 + - !Ref PublicSubnet2 + Tags: + - Key: Name + Value: ServiceLifecycle-DB-SubnetGroup + + # PostgreSQL RDS Instance + PostgreSQLDatabase: + Type: AWS::RDS::DBInstance + DeletionPolicy: Delete + Properties: + DBInstanceIdentifier: servicelifecycle-postgres + DBInstanceClass: db.t3.micro + Engine: postgres + EngineVersion: '15.7' + MasterUsername: !Ref DBUsername + MasterUserPassword: !Ref DBPassword + DBName: !Ref DBName + AllocatedStorage: 20 + StorageType: gp2 + VPCSecurityGroups: + - !Ref DatabaseSecurityGroup + DBSubnetGroupName: !Ref DatabaseSubnetGroup + PubliclyAccessible: true + BackupRetentionPeriod: 0 + MultiAZ: false + StorageEncrypted: false + DeletionProtection: false + Tags: + - Key: Name + Value: ServiceLifecycle-PostgreSQL + + # Lambda function + ServiceLifecycleLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/LambdaWithServiceLifecycle/LambdaWithServiceLifecycle.zip + Timeout: 60 + Handler: swift.bootstrap # ignored by the Swift runtime + Runtime: provided.al2 + MemorySize: 512 + Architectures: + - arm64 + VpcConfig: + SecurityGroupIds: + - !Ref LambdaSecurityGroup + SubnetIds: + - !Ref PublicSubnet1 + - !Ref PublicSubnet2 + Environment: + Variables: + LOG_LEVEL: debug + DB_HOST: !GetAtt PostgreSQLDatabase.Endpoint.Address + DB_USER: !Ref DBUsername + DB_PASSWORD: !Ref DBPassword + DB_NAME: !Ref DBName + Events: + HttpApiEvent: + Type: HttpApi + +Outputs: + # API Gateway endpoint + APIGatewayEndpoint: + Description: API Gateway endpoint URL for the Lambda function + Value: !Sub "https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com" + Export: + Name: !Sub "${AWS::StackName}-APIEndpoint" + + # Database connection details + DatabaseEndpoint: + Description: PostgreSQL database endpoint hostname + Value: !GetAtt PostgreSQLDatabase.Endpoint.Address + Export: + Name: !Sub "${AWS::StackName}-DBEndpoint" + + DatabasePort: + Description: PostgreSQL database port + Value: !GetAtt PostgreSQLDatabase.Endpoint.Port + Export: + Name: !Sub "${AWS::StackName}-DBPort" + + DatabaseName: + Description: PostgreSQL database name + Value: !Ref DBName + Export: + Name: !Sub "${AWS::StackName}-DBName" + + DatabaseUsername: + Description: PostgreSQL database username + Value: !Ref DBUsername + Export: + Name: !Sub "${AWS::StackName}-DBUsername" + + DatabasePassword: + Description: PostgreSQL database password (use with caution) + Value: !Ref DBPassword + Export: + Name: !Sub "${AWS::StackName}-DBPassword" + + # Connection string for easy copy-paste + DatabaseConnectionString: + Description: Complete PostgreSQL connection string + Value: !Sub "postgresql://${DBUsername}:${DBPassword}@${PostgreSQLDatabase.Endpoint.Address}:${PostgreSQLDatabase.Endpoint.Port}/${DBName}" + Export: + Name: !Sub "${AWS::StackName}-DBConnectionString" + + # Individual connection details for manual connection + ConnectionDetails: + Description: Database connection details + Value: !Sub | + Hostname: ${PostgreSQLDatabase.Endpoint.Address} + Port: ${PostgreSQLDatabase.Endpoint.Port} + Database: ${DBName} + Username: ${DBUsername} + Password: ${DBPassword}