From c4c71ccc3b7eb079e90b21848a894c44423d3d4b Mon Sep 17 00:00:00 2001 From: Sebastien Stormacq Date: Sun, 5 Jan 2025 23:21:42 +0100 Subject: [PATCH 01/38] 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 02/38] 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 03/38] 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 04/38] 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 05/38] 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 06/38] 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} From 06231ed4bbaa0bdc188ee765b5521c3190344bb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 21 Jul 2025 21:14:13 +0200 Subject: [PATCH 07/38] fix yam + script --- Examples/ServiceLifecycle/template.yaml | 20 ++++++++++---------- scripts/check-format-linux.sh | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle/template.yaml index b25be3b0..bdb9f68a 100644 --- a/Examples/ServiceLifecycle/template.yaml +++ b/Examples/ServiceLifecycle/template.yaml @@ -7,8 +7,8 @@ Parameters: Type: String Default: postgres Description: Database username - MinLength: 1 - MaxLength: 16 + MinLength: "1" + MaxLength: "16" AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' ConstraintDescription: Must begin with a letter and contain only alphanumeric characters @@ -16,8 +16,8 @@ Parameters: Type: String Default: MySecretPassword123! Description: Database password - MinLength: 8 - MaxLength: 41 + MinLength: "8" + MaxLength: "41" NoEcho: true ConstraintDescription: Must be at least 8 characters long @@ -25,8 +25,8 @@ Parameters: Type: String Default: servicelifecycle Description: Database name - MinLength: 1 - MaxLength: 64 + MinLength: "1" + MaxLength: "64" AllowedPattern: '[a-zA-Z][a-zA-Z0-9]*' ConstraintDescription: Must begin with a letter and contain only alphanumeric characters @@ -62,7 +62,7 @@ Resources: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC - AvailabilityZone: !Select [0, !GetAZs ''] + AvailabilityZone: "!Select [0, !GetAZs '']" CidrBlock: 10.0.1.0/24 MapPublicIpOnLaunch: true Tags: @@ -74,7 +74,7 @@ Resources: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC - AvailabilityZone: !Select [1, !GetAZs ''] + AvailabilityZone: "!Select [1, !GetAZs '']" CidrBlock: 10.0.2.0/24 MapPublicIpOnLaunch: true Tags: @@ -137,7 +137,7 @@ Resources: GroupDescription: Security group for Lambda function VpcId: !Ref VPC SecurityGroupEgress: - - IpProtocol: -1 + - IpProtocol: "-1" CidrIp: 0.0.0.0/0 Description: Allow all outbound traffic Tags: @@ -168,7 +168,7 @@ Resources: MasterUsername: !Ref DBUsername MasterUserPassword: !Ref DBPassword DBName: !Ref DBName - AllocatedStorage: 20 + AllocatedStorage: "20" StorageType: gp2 VPCSecurityGroups: - !Ref DatabaseSecurityGroup diff --git a/scripts/check-format-linux.sh b/scripts/check-format-linux.sh index a5b06c49..57393937 100755 --- a/scripts/check-format-linux.sh +++ b/scripts/check-format-linux.sh @@ -34,7 +34,7 @@ echo "Downloading check-swift-format.sh" curl -s ${CHECK_FORMAT_SCRIPT} > format.sh && chmod u+x format.sh echo "Running check-swift-format.sh" -/usr/local/bin/container run --rm -v "$(pwd):/workspace" -w /workspace ${SWIFT_IMAGE} bash -clx "./format.sh" +/usr/local/bin/docker run --rm -v "$(pwd):/workspace" -w /workspace ${SWIFT_IMAGE} bash -clx "./format.sh" echo "Cleaning up" rm format.sh @@ -46,7 +46,7 @@ echo "Downloading yamllint.yml" curl -s ${YAML_LINT} > yamllint.yml echo "Running yamllint" -/usr/local/bin/container run --rm -v "$(pwd):/workspace" -w /workspace ${YAML_IMAGE} bash -clx "apt-get -qq update && apt-get -qq -y install yamllint && yamllint --strict --config-file /workspace/yamllint.yml .github" +/usr/local/bin/docker run --rm -v "$(pwd):/workspace" -w /workspace ${YAML_IMAGE} bash -clx "apt-get -qq update && apt-get -qq -y install yamllint && yamllint --strict --config-file /workspace/yamllint.yml .github" echo "Cleaning up" rm yamllint.yml From 52a7c8c54893b18dc4077084399c4a98c6a5aa34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 21 Jul 2025 21:18:37 +0200 Subject: [PATCH 08/38] fix soundness --- Examples/ServiceLifecycle/README.md | 2 +- Examples/ServiceLifecycle/deploy.sh | 13 +++++++++++++ Examples/ServiceLifecycle/template.yaml | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Examples/ServiceLifecycle/README.md b/Examples/ServiceLifecycle/README.md index 837307be..c9d1a48e 100644 --- a/Examples/ServiceLifecycle/README.md +++ b/Examples/ServiceLifecycle/README.md @@ -188,7 +188,7 @@ sam delete --stack-name servicelifecycle-stack ### Database connection timeout -The PostgreSQL client may hang if the database is unreachable. This is a known issue with PostgresNIO. Ensure: +The PostgreSQL client may freeze 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 diff --git a/Examples/ServiceLifecycle/deploy.sh b/Examples/ServiceLifecycle/deploy.sh index 8f41e02e..430138d3 100755 --- a/Examples/ServiceLifecycle/deploy.sh +++ b/Examples/ServiceLifecycle/deploy.sh @@ -1,4 +1,17 @@ #!/bin/bash +#===----------------------------------------------------------------------===# +# +# 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 +# +#===----------------------------------------------------------------------===# # ServiceLifecycle Lambda Deployment Script set -e diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle/template.yaml index bdb9f68a..d88da6b7 100644 --- a/Examples/ServiceLifecycle/template.yaml +++ b/Examples/ServiceLifecycle/template.yaml @@ -11,7 +11,7 @@ Parameters: 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! @@ -20,7 +20,7 @@ Parameters: MaxLength: "41" NoEcho: true ConstraintDescription: Must be at least 8 characters long - + DBName: Type: String Default: servicelifecycle From 123bc6c31f60e6adf5f074c1620c83212b2c6b60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 21 Jul 2025 21:21:03 +0200 Subject: [PATCH 09/38] fix soundness --- Examples/ServiceLifecycle/deploy.sh | 26 ++++++++++++------------ Examples/ServiceLifecycle/samconfig.toml | 14 +++++++++++++ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/Examples/ServiceLifecycle/deploy.sh b/Examples/ServiceLifecycle/deploy.sh index 430138d3..02a33452 100755 --- a/Examples/ServiceLifecycle/deploy.sh +++ b/Examples/ServiceLifecycle/deploy.sh @@ -1,17 +1,17 @@ #!/bin/bash -#===----------------------------------------------------------------------===# -# -# 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 -# -#===----------------------------------------------------------------------===# +##===----------------------------------------------------------------------===## +## +## 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 +## +##===----------------------------------------------------------------------===## # ServiceLifecycle Lambda Deployment Script set -e diff --git a/Examples/ServiceLifecycle/samconfig.toml b/Examples/ServiceLifecycle/samconfig.toml index 4171fc12..cbeab1c1 100644 --- a/Examples/ServiceLifecycle/samconfig.toml +++ b/Examples/ServiceLifecycle/samconfig.toml @@ -1,3 +1,17 @@ +##===----------------------------------------------------------------------===## +## +## 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 +## +##===----------------------------------------------------------------------===## + # SAM configuration file for ServiceLifecycle example version = 0.1 From 4679fe1d9c0050b8b2be5478019c7d1bbe0969d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 21 Jul 2025 21:25:01 +0200 Subject: [PATCH 10/38] fix soundness --- .licenseignore | 3 ++- Examples/ServiceLifecycle/samconfig.toml | 14 -------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/.licenseignore b/.licenseignore index d47f45a2..13178627 100644 --- a/.licenseignore +++ b/.licenseignore @@ -34,4 +34,5 @@ Package.resolved *.yml **/.npmignore **/*.json -**/*.txt \ No newline at end of file +**/*.txt +*/toml \ No newline at end of file diff --git a/Examples/ServiceLifecycle/samconfig.toml b/Examples/ServiceLifecycle/samconfig.toml index cbeab1c1..4171fc12 100644 --- a/Examples/ServiceLifecycle/samconfig.toml +++ b/Examples/ServiceLifecycle/samconfig.toml @@ -1,17 +1,3 @@ -##===----------------------------------------------------------------------===## -## -## 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 -## -##===----------------------------------------------------------------------===## - # SAM configuration file for ServiceLifecycle example version = 0.1 From e35c84ee54166fcbac9adccea4e60944d7406a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 21 Jul 2025 21:33:32 +0200 Subject: [PATCH 11/38] Add servicelifecycle to CI --- .github/workflows/pull_request.yml | 2 +- Examples/Streaming/samconfig.toml | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 Examples/Streaming/samconfig.toml diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 058a2888..d4976beb 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,7 +36,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'ServiceLifecycle', 'Streaming', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true diff --git a/Examples/Streaming/samconfig.toml b/Examples/Streaming/samconfig.toml new file mode 100644 index 00000000..6601b7de --- /dev/null +++ b/Examples/Streaming/samconfig.toml @@ -0,0 +1,8 @@ +version = 0.1 +[default.deploy.parameters] +stack_name = "StreamingNumbers" +resolve_s3 = true +s3_prefix = "StreamingNumbers" +region = "us-east-1" +capabilities = "CAPABILITY_IAM" +image_repositories = [] From 1de4acab5e3f1d7bdc952ecd4237a1e40e6a1458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 21 Jul 2025 21:34:53 +0200 Subject: [PATCH 12/38] fix licenseignore --- .licenseignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.licenseignore b/.licenseignore index 13178627..acc480a8 100644 --- a/.licenseignore +++ b/.licenseignore @@ -35,4 +35,4 @@ Package.resolved **/.npmignore **/*.json **/*.txt -*/toml \ No newline at end of file +*.toml \ No newline at end of file From 9bbee05f2ab4291b2e4c38441d73a6d9194f8cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Mon, 21 Jul 2025 21:45:04 +0200 Subject: [PATCH 13/38] fix syntax error in yaml --- Examples/ServiceLifecycle/template.yaml | 66 ++++++++++++------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle/template.yaml index d88da6b7..d1e5c33c 100644 --- a/Examples/ServiceLifecycle/template.yaml +++ b/Examples/ServiceLifecycle/template.yaml @@ -62,7 +62,7 @@ Resources: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC - AvailabilityZone: "!Select [0, !GetAZs '']" + AvailabilityZone: !Select [0, !GetAZs ''] CidrBlock: 10.0.1.0/24 MapPublicIpOnLaunch: true Tags: @@ -74,7 +74,7 @@ Resources: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC - AvailabilityZone: "!Select [1, !GetAZs '']" + AvailabilityZone: !Select [1, !GetAZs ''] CidrBlock: 10.0.2.0/24 MapPublicIpOnLaunch: true Tags: @@ -183,40 +183,40 @@ Resources: 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 + # 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" + # 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: From a5ad767bd88dddeb0d58e5ad570b46f3c8640f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 17:03:01 +0200 Subject: [PATCH 14/38] fix error on CTRL-C --- Sources/AWSLambdaRuntime/Lambda+LocalServer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index bdf8f034..0dfcb456 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -106,7 +106,7 @@ internal struct LambdaHTTPServer { eventLoopGroup: MultiThreadedEventLoopGroup = .singleton, logger: Logger, _ closure: sending @escaping () async throws -> Result - ) async throws -> Result { + ) async throws -> Swift.Result { let channel = try await ServerBootstrap(group: eventLoopGroup) .serverChannelOption(.backlog, value: 256) .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) @@ -224,7 +224,7 @@ internal struct LambdaHTTPServer { } logger.info("Server shutting down") - return try result.get() + return result // ignore errors here, we are shutting down anyway } /// This method handles individual TCP connections From 554b19643d03207cceee9b0dcf386ea054ad74e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 17:03:18 +0200 Subject: [PATCH 15/38] update example --- .../ServiceLifecycle/Sources/Lambda.swift | 49 ++- .../Sources/RDSCertificates/RootRDSCert.swift | 22 ++ .../RDSCertificates/eu-central-1.swift | 92 +++++ .../Sources/RDSCertificates/us-east-1.swift | 92 +++++ .../Sources/RootRDSCert.swift | 95 ----- .../ServiceLifecycle/Sources/Timeout.swift | 67 ++++ Examples/ServiceLifecycle/localdb.sh | 44 +++ Examples/ServiceLifecycle/template.yaml | 326 +++++++++++++----- 8 files changed, 598 insertions(+), 189 deletions(-) create mode 100644 Examples/ServiceLifecycle/Sources/RDSCertificates/RootRDSCert.swift create mode 100644 Examples/ServiceLifecycle/Sources/RDSCertificates/eu-central-1.swift create mode 100644 Examples/ServiceLifecycle/Sources/RDSCertificates/us-east-1.swift delete mode 100644 Examples/ServiceLifecycle/Sources/RootRDSCert.swift create mode 100644 Examples/ServiceLifecycle/Sources/Timeout.swift create mode 100644 Examples/ServiceLifecycle/localdb.sh diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index 7bb54cf6..ffe6b830 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -17,6 +17,11 @@ import Logging import PostgresNIO import ServiceLifecycle +struct User: Codable { + let id: Int + let username: String +} + @main struct LambdaFunction { @@ -33,7 +38,8 @@ struct LambdaFunction { host: Lambda.env("DB_HOST") ?? "localhost", user: Lambda.env("DB_USER") ?? "postgres", password: Lambda.env("DB_PASSWORD") ?? "secret", - dbName: Lambda.env("DB_NAME") ?? "test" + dbName: Lambda.env("DB_NAME") ?? "test", + logger: logger ) } private func main() async throws { @@ -47,7 +53,7 @@ struct LambdaFunction { /// of the PGClient together with the LambdaRuntime let serviceGroup = ServiceGroup( services: [pgClient, runtime], - // gracefulShutdownSignals: [.sigterm, .sigint], // add SIGINT for CTRL+C in local testing + gracefulShutdownSignals: [.sigterm], cancellationSignals: [.sigint], logger: logger ) @@ -57,33 +63,52 @@ struct LambdaFunction { } - private func handler(event: String, context: LambdaContext) async -> String { + private func handler(event: String, context: LambdaContext) async -> [User] { + + // input event is ignored here + + var result: [User] = [] 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)") + // this is why there is a timeout, as suggested by + // https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 + logger.info("Connecting to the database") + result = try await timeout(deadline: .seconds(3)) { + let rows = try await pgClient.query("SELECT id, username FROM users") + var users: [User] = [] + for try await (id, username) in rows.decode((Int, String).self) { + logger.info("Adding \(id) : \(username)") + users.append(User(id: id, username: username)) + } + return users } } catch { logger.error("PG Error: \(error)") } - return "Done" + return result } private static func preparePostgresClient( host: String, user: String, password: String, - dbName: String + dbName: String, + logger: Logger ) throws -> PostgresClient { - var tlsConfig = TLSConfiguration.makeClientConfiguration() // Load the root certificate - let rootCert = try NIOSSLCertificate.fromPEMBytes(Array(eu_central_1_bundle_pem.utf8)) + 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 @@ -101,3 +126,7 @@ struct LambdaFunction { return PostgresClient(configuration: config) } } + +public enum LambdaErrors: Error { + case missingRootCertificateForRegion(String) +} diff --git a/Examples/ServiceLifecycle/Sources/RDSCertificates/RootRDSCert.swift b/Examples/ServiceLifecycle/Sources/RDSCertificates/RootRDSCert.swift new file mode 100644 index 00000000..f89c5fbb --- /dev/null +++ b/Examples/ServiceLifecycle/Sources/RDSCertificates/RootRDSCert.swift @@ -0,0 +1,22 @@ +//===----------------------------------------------------------------------===// +// +// 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 rootRDSCertificates = [ + "eu-central-1": eu_central_1_bundle_pem, + "us-east-1": us_east_1_bundle_pem, + // add more regions as needed +] diff --git a/Examples/ServiceLifecycle/Sources/RDSCertificates/eu-central-1.swift b/Examples/ServiceLifecycle/Sources/RDSCertificates/eu-central-1.swift new file mode 100644 index 00000000..e602ebf2 --- /dev/null +++ b/Examples/ServiceLifecycle/Sources/RDSCertificates/eu-central-1.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 +// +//===----------------------------------------------------------------------===// + +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----- + """ diff --git a/Examples/ServiceLifecycle/Sources/RDSCertificates/us-east-1.swift b/Examples/ServiceLifecycle/Sources/RDSCertificates/us-east-1.swift new file mode 100644 index 00000000..f68a6781 --- /dev/null +++ b/Examples/ServiceLifecycle/Sources/RDSCertificates/us-east-1.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 +// +//===----------------------------------------------------------------------===// + +let us_east_1_bundle_pem = """ + -----BEGIN CERTIFICATE----- + MIID/zCCAuegAwIBAgIRAPVSMfFitmM5PhmbaOFoGfUwDQYJKoZIhvcNAQELBQAw + gZcxCzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJ + bmMuMRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEwMC4GA1UEAwwn + QW1hem9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBSU0EyMDQ4IEcxMRAwDgYDVQQH + DAdTZWF0dGxlMCAXDTIxMDUyNTIyMzQ1N1oYDzIwNjEwNTI1MjMzNDU3WjCBlzEL + MAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIEluYy4x + EzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTAwLgYDVQQDDCdBbWF6 + b24gUkRTIHVzLWVhc3QtMSBSb290IENBIFJTQTIwNDggRzExEDAOBgNVBAcMB1Nl + YXR0bGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDu9H7TBeGoDzMr + dxN6H8COntJX4IR6dbyhnj5qMD4xl/IWvp50lt0VpmMd+z2PNZzx8RazeGC5IniV + 5nrLg0AKWRQ2A/lGGXbUrGXCSe09brMQCxWBSIYe1WZZ1iU1IJ/6Bp4D2YEHpXrW + bPkOq5x3YPcsoitgm1Xh8ygz6vb7PsvJvPbvRMnkDg5IqEThapPjmKb8ZJWyEFEE + QRrkCIRueB1EqQtJw0fvP4PKDlCJAKBEs/y049FoOqYpT3pRy0WKqPhWve+hScMd + 6obq8kxTFy1IHACjHc51nrGII5Bt76/MpTWhnJIJrCnq1/Uc3Qs8IVeb+sLaFC8K + DI69Sw6bAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFE7PCopt + lyOgtXX0Y1lObBUxuKaCMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOC + AQEAFj+bX8gLmMNefr5jRJfHjrL3iuZCjf7YEZgn89pS4z8408mjj9z6Q5D1H7yS + jNETVV8QaJip1qyhh5gRzRaArgGAYvi2/r0zPsy+Tgf7v1KGL5Lh8NT8iCEGGXwF + g3Ir+Nl3e+9XUp0eyyzBIjHtjLBm6yy8rGk9p6OtFDQnKF5OxwbAgip42CD75r/q + p421maEDDvvRFR4D+99JZxgAYDBGqRRceUoe16qDzbMvlz0A9paCZFclxeftAxv6 + QlR5rItMz/XdzpBJUpYhdzM0gCzAzdQuVO5tjJxmXhkSMcDP+8Q+Uv6FA9k2VpUV + E/O5jgpqUJJ2Hc/5rs9VkAPXeA== + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + 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 + 0Ws= + -----END CERTIFICATE----- + -----BEGIN CERTIFICATE----- + MIICrjCCAjSgAwIBAgIRAPAlEk8VJPmEzVRRaWvTh2AwCgYIKoZIzj0EAwMwgZYx + CzAJBgNVBAYTAlVTMSIwIAYDVQQKDBlBbWF6b24gV2ViIFNlcnZpY2VzLCBJbmMu + MRMwEQYDVQQLDApBbWF6b24gUkRTMQswCQYDVQQIDAJXQTEvMC0GA1UEAwwmQW1h + em9uIFJEUyB1cy1lYXN0LTEgUm9vdCBDQSBFQ0MzODQgRzExEDAOBgNVBAcMB1Nl + YXR0bGUwIBcNMjEwNTI1MjI0MTU1WhgPMjEyMTA1MjUyMzQxNTVaMIGWMQswCQYD + VQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5jLjETMBEG + A1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExLzAtBgNVBAMMJkFtYXpvbiBS + RFMgdXMtZWFzdC0xIFJvb3QgQ0EgRUNDMzg0IEcxMRAwDgYDVQQHDAdTZWF0dGxl + MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEx5xjrup8II4HOJw15NTnS3H5yMrQGlbj + EDA5MMGnE9DmHp5dACIxmPXPMe/99nO7wNdl7G71OYPCgEvWm0FhdvVUeTb3LVnV + BnaXt32Ek7/oxGk1T+Df03C+W0vmuJ+wo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0G + A1UdDgQWBBTGXmqBWN/1tkSea4pNw0oHrjk2UDAOBgNVHQ8BAf8EBAMCAYYwCgYI + KoZIzj0EAwMDaAAwZQIxAIqqZWCSrIkZ7zsv/FygtAusW6yvlL935YAWYPVXU30m + jkMFLM+/RJ9GMvnO8jHfCgIwB+whlkcItzE9CRQ6CsMo/d5cEHDUu/QW6jSIh9BR + OGh9pTYPVkUbBiKPA7lVVhre + -----END CERTIFICATE----- + """ diff --git a/Examples/ServiceLifecycle/Sources/RootRDSCert.swift b/Examples/ServiceLifecycle/Sources/RootRDSCert.swift deleted file mode 100644 index 23cab9f3..00000000 --- a/Examples/ServiceLifecycle/Sources/RootRDSCert.swift +++ /dev/null @@ -1,95 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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----- - MIIGBDCCA+ygAwIBAgIQM4C8g5iFRucSWdC8EdqHeDANBgkqhkiG9w0BAQwFADCB - mjELMAkGA1UEBhMCVVMxIjAgBgNVBAoMGUFtYXpvbiBXZWIgU2VydmljZXMsIElu - Yy4xEzARBgNVBAsMCkFtYXpvbiBSRFMxCzAJBgNVBAgMAldBMTMwMQYDVQQDDCpB - bWF6b24gUkRTIGV1LWNlbnRyYWwtMSBSb290IENBIFJTQTQwOTYgRzExEDAOBgNV - BAcMB1NlYXR0bGUwIBcNMjEwNTIxMjIyODI2WhgPMjEyMTA1MjEyMzI4MjZaMIGa - MQswCQYDVQQGEwJVUzEiMCAGA1UECgwZQW1hem9uIFdlYiBTZXJ2aWNlcywgSW5j - LjETMBEGA1UECwwKQW1hem9uIFJEUzELMAkGA1UECAwCV0ExMzAxBgNVBAMMKkFt - YXpvbiBSRFMgZXUtY2VudHJhbC0xIFJvb3QgQ0EgUlNBNDA5NiBHMTEQMA4GA1UE - BwwHU2VhdHRsZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANeTsD/u - 6saPiY4Sg0GlJlMXMBltnrcGAEkwq34OKQ0bCXqcoNJ2rcAMmuFC5x9Ho1Y3YzB7 - NO2GpIh6bZaO76GzSv4cnimcv9n/sQSYXsGbPD+bAtnN/RvNW1avt4C0q0/ghgF1 - VFS8JihIrgPYIArAmDtGNEdl5PUrdi9y6QGggbRfidMDdxlRdZBe1C18ZdgERSEv - UgSTPRlVczONG5qcQkUGCH83MMqL5MKQiby/Br5ZyPq6rxQMwRnQ7tROuElzyYzL - 7d6kke+PNzG1mYy4cbYdjebwANCtZ2qYRSUHAQsOgybRcSoarv2xqcjO9cEsDiRU - l97ToadGYa4VVERuTaNZxQwrld4mvzpyKuirqZltOqg0eoy8VUsaRPL3dc5aChR0 - dSrBgRYmSAClcR2/2ZCWpXemikwgt031Dsc0A/+TmVurrsqszwbr0e5xqMow9LzO - MI/JtLd0VFtoOkL/7GG2tN8a+7gnLFxpv+AQ0DH5n4k/BY/IyS+H1erqSJhOTQ11 - vDOFTM5YplB9hWV9fp5PRs54ILlHTlZLpWGs3I2BrJwzRtg/rOlvsosqcge9ryai - AKm2j+JBg5wJ19R8oxRy8cfrNTftZePpISaLTyV2B16w/GsSjqixjTQe9LRN2DHk - cC+HPqYyzW2a3pUVyTGHhW6a7YsPBs9yzt6hAgMBAAGjQjBAMA8GA1UdEwEB/wQF - MAMBAf8wHQYDVR0OBBYEFIqA8QkOs2cSirOpCuKuOh9VDfJfMA4GA1UdDwEB/wQE - AwIBhjANBgkqhkiG9w0BAQwFAAOCAgEAOUI90mEIsa+vNJku0iUwdBMnHiO4gm7E - 5JloP7JG0xUr7d0hypDorMM3zVDAL+aZRHsq8n934Cywj7qEp1304UF6538ByGdz - tkfacJsUSYfdlNJE9KbA4T+U+7SNhj9jvePpVjdQbhgzxITE9f8CxY/eM40yluJJ - PhbaWvOiRagzo74wttlcDerzLT6Y/JrVpWhnB7IY8HvzK+BwAdaCsBUPC3HF+kth - CIqLq7J3YArTToejWZAp5OOI6DLPM1MEudyoejL02w0jq0CChmZ5i55ElEMnapRX - 7GQTARHmjgAOqa95FjbHEZzRPqZ72AtZAWKFcYFNk+grXSeWiDgPFOsq6mDg8DDB - 0kfbYwKLFFCC9YFmYzR2YrWw2NxAScccUc2chOWAoSNHiqBbHR8ofrlJSWrtmKqd - YRCXzn8wqXnTS3NNHNccqJ6dN+iMr9NGnytw8zwwSchiev53Fpc1mGrJ7BKTWH0t - ZrA6m32wzpMymtKozlOPYoE5mtZEzrzHEXfa44Rns7XIHxVQSXVWyBHLtIsZOrvW - U5F41rQaFEpEeUQ7sQvqUoISfTUVRNDn6GK6YaccEhCji14APLFIvhRQUDyYMIiM - 4vll0F/xgVRHTgDVQ8b8sxdhSYlqB4Wc2Ym41YRz+X2yPqk3typEZBpc4P5Tt1/N - 89cEIGdbjsA= - -----END CERTIFICATE----- - """ diff --git a/Examples/ServiceLifecycle/Sources/Timeout.swift b/Examples/ServiceLifecycle/Sources/Timeout.swift new file mode 100644 index 00000000..6a8dc5dc --- /dev/null +++ b/Examples/ServiceLifecycle/Sources/Timeout.swift @@ -0,0 +1,67 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +// as suggested by https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 +func timeout( + deadline: Duration, + _ closure: @escaping @Sendable () async throws -> Success +) async throws -> Success { + + let clock = ContinuousClock() + + let result = await withTaskGroup(of: TimeoutResult.self, returning: Result.self) { + taskGroup in + taskGroup.addTask { + do { + try await clock.sleep(until: clock.now + deadline, tolerance: nil) + return .deadlineHit + } catch { + return .deadlineCancelled + } + } + + taskGroup.addTask { + do { + let success = try await closure() + return .workFinished(.success(success)) + } catch let error { + return .workFinished(.failure(error)) + } + } + + var r: Swift.Result? + while let taskResult = await taskGroup.next() { + switch taskResult { + case .deadlineCancelled: + continue // loop + + case .deadlineHit: + taskGroup.cancelAll() + + case .workFinished(let result): + taskGroup.cancelAll() + r = result + } + } + return r! + } + + return try result.get() +} + +enum TimeoutResult { + case deadlineHit + case deadlineCancelled + case workFinished(Result) +} diff --git a/Examples/ServiceLifecycle/localdb.sh b/Examples/ServiceLifecycle/localdb.sh new file mode 100644 index 00000000..f091132d --- /dev/null +++ b/Examples/ServiceLifecycle/localdb.sh @@ -0,0 +1,44 @@ +##===----------------------------------------------------------------------===## +## +## 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 +## +##===----------------------------------------------------------------------===## + +# For testing purposes, this script sets up a local PostgreSQL database using Docker. + +# Create a named volume for PostgreSQL data +docker volume create pgdata + +# Run PostgreSQL container with the volume mounted +docker run -d \ + --name postgres-db \ + -e POSTGRES_PASSWORD=secret \ + -e POSTGRES_USER=postgres \ + -e POSTGRES_DB=test \ + -p 5432:5432 \ + -v pgdata:/var/lib/postgresql/data \ + postgres:latest + +# Stop the container +docker stop postgres-db + +# Start it again (data persists) +docker start postgres-db + +# Connect to the database using psql in a new container +docker run -it --rm --network host \ + -e PGPASSWORD=secret \ + postgres:latest \ + psql -h localhost -U postgres -d test + +# Alternative: Connect using the postgres-db container itself +docker exec -it postgres-db psql -U postgres -d test + diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle/template.yaml index d1e5c33c..f79c3cbe 100644 --- a/Examples/ServiceLifecycle/template.yaml +++ b/Examples/ServiceLifecycle/template.yaml @@ -3,23 +3,6 @@ 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 @@ -31,7 +14,7 @@ Parameters: ConstraintDescription: Must begin with a letter and contain only alphanumeric characters Resources: - # VPC for RDS (required for public access configuration) + # VPC for RDS and Lambda VPC: Type: AWS::EC2::VPC Properties: @@ -57,7 +40,7 @@ Resources: InternetGatewayId: !Ref InternetGateway VpcId: !Ref VPC - # Public Subnet 1 + # Public Subnet 1 for Lambda PublicSubnet1: Type: AWS::EC2::Subnet Properties: @@ -69,7 +52,7 @@ Resources: - Key: Name Value: ServiceLifecycle-Public-Subnet-1 - # Public Subnet 2 (required for RDS subnet group) + # Public Subnet 2 for Lambda PublicSubnet2: Type: AWS::EC2::Subnet Properties: @@ -80,6 +63,30 @@ Resources: Tags: - Key: Name Value: ServiceLifecycle-Public-Subnet-2 + + # Private Subnet 1 for RDS + PrivateSubnet1: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [0, !GetAZs ''] + CidrBlock: 10.0.3.0/24 + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: ServiceLifecycle-Private-Subnet-1 + + # Private Subnet 2 for RDS + PrivateSubnet2: + Type: AWS::EC2::Subnet + Properties: + VpcId: !Ref VPC + AvailabilityZone: !Select [1, !GetAZs ''] + CidrBlock: 10.0.4.0/24 + MapPublicIpOnLaunch: false + Tags: + - Key: Name + Value: ServiceLifecycle-Private-Subnet-2 # Route Table for public subnets PublicRouteTable: @@ -111,20 +118,60 @@ Resources: Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet2 + + # NAT Gateway for private subnets + NatGatewayEIP: + Type: AWS::EC2::EIP + DependsOn: InternetGatewayAttachment + Properties: + Domain: vpc + + NatGateway: + Type: AWS::EC2::NatGateway + Properties: + AllocationId: !GetAtt NatGatewayEIP.AllocationId + SubnetId: !Ref PublicSubnet1 + Tags: + - Key: Name + Value: ServiceLifecycle-NAT-Gateway + + # Route Table for private subnets + PrivateRouteTable: + Type: AWS::EC2::RouteTable + Properties: + VpcId: !Ref VPC + Tags: + - Key: Name + Value: ServiceLifecycle-Private-Routes + + # Route to NAT Gateway + DefaultPrivateRoute: + Type: AWS::EC2::Route + Properties: + RouteTableId: !Ref PrivateRouteTable + DestinationCidrBlock: 0.0.0.0/0 + NatGatewayId: !Ref NatGateway + + # Associate private subnets with route table + PrivateSubnet1RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PrivateRouteTable + SubnetId: !Ref PrivateSubnet1 + + PrivateSubnet2RouteTableAssociation: + Type: AWS::EC2::SubnetRouteTableAssociation + Properties: + RouteTableId: !Ref PrivateRouteTable + SubnetId: !Ref PrivateSubnet2 - # Security Group for RDS (allows public access) + # Security Group for RDS DatabaseSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupName: ServiceLifecycle-DB-SG - GroupDescription: Security group for PostgreSQL database - allows public access + GroupDescription: Security group for PostgreSQL database 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 @@ -137,9 +184,16 @@ Resources: GroupDescription: Security group for Lambda function VpcId: !Ref VPC SecurityGroupEgress: - - IpProtocol: "-1" + - IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + CidrIp: 10.0.0.0/16 + Description: Allow PostgreSQL access within VPC only + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 CidrIp: 0.0.0.0/0 - Description: Allow all outbound traffic + Description: Allow HTTPS outbound traffic Tags: - Key: Name Value: ServiceLifecycle-Lambda-SecurityGroup @@ -150,12 +204,105 @@ Resources: Properties: DBSubnetGroupDescription: Subnet group for PostgreSQL database SubnetIds: - - !Ref PublicSubnet1 - - !Ref PublicSubnet2 + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 Tags: - Key: Name Value: ServiceLifecycle-DB-SubnetGroup + # Database credentials stored in Secrets Manager + DatabaseSecret: + Type: AWS::SecretsManager::Secret + Properties: + Name: !Sub "${AWS::StackName}-db-credentials" + Description: RDS database credentials + GenerateSecretString: + SecretStringTemplate: '{"username":"postgres"}' + GenerateStringKey: "password" + PasswordLength: 16 + ExcludeCharacters: '"@/\\' + + # Database Security Group Ingress Rule (added separately to avoid circular dependency) + DatabaseSecurityGroupIngress: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref DatabaseSecurityGroup + IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + SourceSecurityGroupId: !Ref LambdaSecurityGroup + Description: Allow PostgreSQL access from Lambda security group + + # Security Group for SSM VPC Endpoints + SSMEndpointSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: ServiceLifecycle-SSM-Endpoint-SG + GroupDescription: Security group for SSM VPC endpoints + VpcId: !Ref VPC + SecurityGroupIngress: + - IpProtocol: tcp + FromPort: 443 + ToPort: 443 + CidrIp: 0.0.0.0/0 + Description: Allow HTTPS inbound for SSM connections + Tags: + - Key: Name + Value: ServiceLifecycle-SSM-Endpoint-SecurityGroup + + # Add ingress rule to Database Security Group for SSM connections + DatabaseSecurityGroupIngressSSM: + Type: AWS::EC2::SecurityGroupIngress + Properties: + GroupId: !Ref DatabaseSecurityGroup + IpProtocol: tcp + FromPort: 5432 + ToPort: 5432 + SourceSecurityGroupId: !Ref SSMEndpointSecurityGroup + Description: Allow PostgreSQL access from SSM endpoints + + # SSM VPC Endpoint + SSMEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcEndpointType: Interface + PrivateDnsEnabled: true + ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm" + VpcId: !Ref VPC + SubnetIds: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + SecurityGroupIds: + - !Ref SSMEndpointSecurityGroup + + # SSM Messages VPC Endpoint + SSMMessagesEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcEndpointType: Interface + PrivateDnsEnabled: true + ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages" + VpcId: !Ref VPC + SubnetIds: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + SecurityGroupIds: + - !Ref SSMEndpointSecurityGroup + + # EC2 Messages VPC Endpoint + EC2MessagesEndpoint: + Type: AWS::EC2::VPCEndpoint + Properties: + VpcEndpointType: Interface + PrivateDnsEnabled: true + ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages" + VpcId: !Ref VPC + SubnetIds: + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 + SecurityGroupIds: + - !Ref SSMEndpointSecurityGroup + # PostgreSQL RDS Instance PostgreSQLDatabase: Type: AWS::RDS::DBInstance @@ -165,58 +312,59 @@ Resources: DBInstanceClass: db.t3.micro Engine: postgres EngineVersion: '15.7' - MasterUsername: !Ref DBUsername - MasterUserPassword: !Ref DBPassword + MasterUsername: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:username}}']] + MasterUserPassword: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:password}}']] DBName: !Ref DBName AllocatedStorage: "20" StorageType: gp2 VPCSecurityGroups: - !Ref DatabaseSecurityGroup DBSubnetGroupName: !Ref DatabaseSubnetGroup - PubliclyAccessible: true + PubliclyAccessible: false BackupRetentionPeriod: 0 MultiAZ: false - StorageEncrypted: false + StorageEncrypted: true 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 + ServiceLifecycleLambda: + Type: AWS::Serverless::Function + Properties: + CodeUri: ./LambdaWithServiceLifecycle.zip + #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: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:username}}']] + DB_PASSWORD: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:password}}']] + 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" + 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: @@ -237,24 +385,18 @@ Outputs: 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 + DatabaseSecretArn: + Description: ARN of the secret containing database credentials + Value: !Ref DatabaseSecret Export: - Name: !Sub "${AWS::StackName}-DBPassword" + Name: !Sub "${AWS::StackName}-DBSecretArn" - # Connection string for easy copy-paste - DatabaseConnectionString: - Description: Complete PostgreSQL connection string - Value: !Sub "postgresql://${DBUsername}:${DBPassword}@${PostgreSQLDatabase.Endpoint.Address}:${PostgreSQLDatabase.Endpoint.Port}/${DBName}" + # Connection string instructions + DatabaseConnectionInstructions: + Description: Instructions to get the connection string + Value: !Sub "Use 'aws secretsmanager get-secret-value --secret-id ${DatabaseSecret}' to retrieve credentials" Export: - Name: !Sub "${AWS::StackName}-DBConnectionString" + Name: !Sub "${AWS::StackName}-DBConnectionInstructions" # Individual connection details for manual connection ConnectionDetails: @@ -263,5 +405,21 @@ Outputs: Hostname: ${PostgreSQLDatabase.Endpoint.Address} Port: ${PostgreSQLDatabase.Endpoint.Port} Database: ${DBName} - Username: ${DBUsername} - Password: ${DBPassword} + Credentials: Use AWS Secrets Manager to retrieve username and password + + # SSM VPC Endpoints + SSMEndpoints: + Description: SSM VPC Endpoints for CloudShell access + Value: !Sub | + SSM Endpoint: ${SSMEndpoint} + SSM Messages Endpoint: ${SSMMessagesEndpoint} + EC2 Messages Endpoint: ${EC2MessagesEndpoint} + + # CloudShell Connection Instructions + CloudShellInstructions: + Description: Instructions for connecting from CloudShell + Value: !Sub | + 1. Open AWS CloudShell + 2. Run: aws secretsmanager get-secret-value --secret-id ${DatabaseSecret} --query 'SecretString' --output text + 3. Parse the JSON output to get username and password + 4. Connect using: PGPASSWORD= psql -h ${PostgreSQLDatabase.Endpoint.Address} -U -d ${DBName} From a0959b0485a3179d55e530067e882254b4a52c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 17:16:47 +0200 Subject: [PATCH 16/38] add readme and infrastructure doc --- Examples/ServiceLifecycle/INFRASTRUCTURE.md | 154 +++++++++++++++++++ Examples/ServiceLifecycle/README.md | 159 ++++++++++++++------ 2 files changed, 271 insertions(+), 42 deletions(-) create mode 100644 Examples/ServiceLifecycle/INFRASTRUCTURE.md diff --git a/Examples/ServiceLifecycle/INFRASTRUCTURE.md b/Examples/ServiceLifecycle/INFRASTRUCTURE.md new file mode 100644 index 00000000..07e82988 --- /dev/null +++ b/Examples/ServiceLifecycle/INFRASTRUCTURE.md @@ -0,0 +1,154 @@ +# Infrastructure Documentation + +This document describes the AWS infrastructure defined in `template.yaml` for the ServiceLifecycle Lambda with PostgreSQL example. + +## Architecture Overview + +The infrastructure consists of a secure VPC setup with public and private subnets, a Lambda function in public subnets, and an RDS PostgreSQL database in private subnets. The architecture follows AWS best practices for security and connectivity. + +```mermaid +graph TD + subgraph "AWS Cloud" + subgraph "VPC (10.0.0.0/16)" + subgraph "Public Subnets" + Lambda["Lambda Function"] + NAT["NAT Gateway"] + IGW["Internet Gateway"] + end + + subgraph "Private Subnets" + RDS["PostgreSQL RDS"] + SSM1["SSM Endpoint"] + SSM2["SSM Messages Endpoint"] + SSM3["EC2 Messages Endpoint"] + end + + Lambda -- "Egress to DB (5432)" --> RDS + Lambda -- "Egress to AWS APIs (443)" --> Internet + + RDS -- "Ingress from Lambda (5432)" --> Lambda + RDS -- "Ingress from SSM (5432)" --> SSM1 + + NAT -- "Outbound traffic" --> IGW + IGW -- "Internet Access" --> Internet + end + + SecretsManager["Secrets Manager"] + APIGateway["API Gateway"] + EC2["Amazon EC2"] + + APIGateway --> Lambda + Lambda --> SecretsManager + EC2 -- "Via SSM Endpoints" --> SSM1 + SSM1 --> RDS + end + + User["User"] --> APIGateway + Admin["Admin"] --> EC2 +``` + +## Key Components + +### Networking + +1. **VPC**: A dedicated VPC with CIDR block `10.0.0.0/16` +2. **Subnets**: + - Public Subnets (10.0.1.0/24, 10.0.2.0/24): For Lambda and NAT Gateway + - Private Subnets (10.0.3.0/24, 10.0.4.0/24): For RDS and VPC endpoints +3. **Internet Gateway**: Provides internet access for public subnets +4. **NAT Gateway**: Allows outbound internet access from private subnets +5. **Route Tables**: Separate route tables for public and private subnets + +### Security + +1. **Security Groups**: + - **Lambda Security Group**: Restricts outbound traffic to: + - PostgreSQL (5432) within the VPC + - HTTPS (443) to the internet + - **Database Security Group**: Allows inbound PostgreSQL connections only from: + - Lambda Security Group + - SSM Endpoint Security Group + - **SSM Endpoint Security Group**: Allows HTTPS inbound for SSM connections + +2. **Encryption**: + - RDS storage encryption enabled + - SSL/TLS for database connections with certificate verification + - Secrets Manager for secure credential storage + +### Compute & Database + +1. **Lambda Function**: + - Runtime: provided.al2 (Swift) + - Memory: 512MB + - Timeout: 60 seconds + - VPC integration with public subnets + +2. **RDS PostgreSQL**: + - Instance class: db.t3.micro + - Engine version: 15.7 + - Storage: 20GB gp2 + - Placed in private subnets + - Not publicly accessible + +### Access & Management + +1. **Secrets Manager**: + - Stores database credentials + - Auto-generates secure password + - Referenced by Lambda and RDS + +2. **VPC Endpoints**: + - SSM Endpoint + - SSM Messages Endpoint + - EC2 Messages Endpoint + - Enables CloudShell access to private resources + +3. **Function URL**: + - HTTP endpoint for invoking Lambda function + +## Security Considerations + +1. **Network Isolation**: Database is in private subnets, not directly accessible from the internet +2. **Least Privilege**: Security groups follow principle of least privilege +3. **Encryption**: Data at rest is encrypted +4. **Secure Credentials**: No hardcoded credentials, using Secrets Manager +5. **SSL/TLS**: Database connections use SSL/TLS with certificate verification + +## Accessing the Database + +The database is in a private subnet and not directly accessible from the internet. To connect: + +1. **From Lambda**: Direct connection through VPC networking +2. **From Amazon EC2**: Through SSM VPC endpoints +3. **From your local machine**: Options include: + - SSH tunnel through a bastion host + - AWS Session Manager port forwarding + - AWS Cloud9 environment in the same VPC + +## Deployment + +Deploy this infrastructure using AWS SAM: + +```bash +sam deploy +``` + +## Outputs + +After deployment, the following information is available in CloudFormation outputs: + +- API Gateway endpoint URL +- Database endpoint hostname and port +- Database name +- Secret ARN for retrieving credentials +- Connection instructions + +## Cost Optimization + +The template uses cost-effective resources: +- db.t3.micro RDS instance (eligible for free tier) +- Minimal storage allocation (20GB) +- Single-AZ deployment +- No automated backups + +For production workloads, consider adjusting these settings based on your requirements. \ No newline at end of file diff --git a/Examples/ServiceLifecycle/README.md b/Examples/ServiceLifecycle/README.md index c9d1a48e..6a9242b0 100644 --- a/Examples/ServiceLifecycle/README.md +++ b/Examples/ServiceLifecycle/README.md @@ -1,13 +1,33 @@ # 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. +This example demonstrates a Swift Lambda function that uses ServiceLifecycle to manage a PostgreSQL connection. The function connects to an RDS PostgreSQL database in private subnets 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 +- **Swift Lambda Function**: Uses ServiceLifecycle to manage PostgreSQL client lifecycle, deployed in public subnets +- **PostgreSQL RDS**: Database instance in private subnets with SSL/TLS encryption +- **Function URL**: HTTP endpoint to invoke the Lambda function +- **VPC**: Custom VPC with public subnets for Lambda/NAT Gateway and private subnets for RDS +- **Security**: SSL/TLS connections with RDS root certificate verification, secure networking with security groups +- **Timeout Handling**: 3-second timeout mechanism to prevent database connection hangs +- **VPC Endpoints**: SSM endpoints for administrative access to private resources +- **Secrets Manager**: Secure credential storage and management + +For detailed infrastructure information, see `INFRASTRUCTURE.md`. + +## Implementation Details + +The Lambda function demonstrates several key concepts: + +1. **ServiceLifecycle Integration**: The PostgreSQL client and Lambda runtime are managed together using ServiceLifecycle, ensuring proper initialization and cleanup. + +2. **SSL/TLS Security**: Connections to RDS use SSL/TLS with full certificate verification using region-specific RDS root certificates. + +3. **Timeout Protection**: A custom timeout mechanism prevents the function from hanging when the database is unreachable (addresses PostgresNIO issue #489). + +4. **Structured Response**: Returns a JSON array of `User` objects, making it suitable for API integration. + +5. **Error Handling**: Comprehensive error handling for database connections, queries, and certificate loading. ## Prerequisites @@ -18,7 +38,7 @@ This example demonstrates a Swift Lambda function that uses ServiceLifecycle to ## Database Schema -The Lambda function expects a `users` table with the following structure: +The Lambda function expects a `users` table with the following structure and returns results as `User` objects: ```sql CREATE TABLE users ( @@ -30,6 +50,23 @@ CREATE TABLE users ( INSERT INTO users (username) VALUES ('alice'), ('bob'), ('charlie'); ``` +The Swift `User` model: +```swift +struct User: Codable { + let id: Int + let username: String +} +``` + +## Environment Variables + +The Lambda function uses the following environment variables for database connection: +- `DB_HOST`: Database hostname (set by CloudFormation from RDS endpoint) +- `DB_USER`: Database username (retrieved from Secrets Manager) +- `DB_PASSWORD`: Database password (retrieved from Secrets Manager) +- `DB_NAME`: Database name (defaults to "test") +- `AWS_REGION`: AWS region for selecting the correct RDS root certificate + ## Deployment ### Option 1: Using the deployment script @@ -50,15 +87,6 @@ INSERT INTO users (username) VALUES ('alice'), ('bob'), ('charlie'); 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: @@ -79,27 +107,43 @@ The output will include: ## Connecting to the Database -### Using psql +### Important: Database Access + +The PostgreSQL database is deployed in **private subnets** and is **not directly accessible** from the internet. This follows AWS security best practices. + +### From Amazon EC2 (Recommended for testing) + +an Amazon EC2 instance deployed in the publci subnet of the VPC can connect through the VPC endpoints configured in the template: ```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 +# Get the database password from Secrets Manager +SECRET_ARN=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseSecretArn`].OutputValue' --output text) +DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" --query 'SecretString' --output text | jq -r '.password') + +# Connect with psql on Amazon EC2 psql -h $DB_HOST -U $DB_USER -d $DB_NAME ``` -### Using connection string +### From your local machine + +Since the database is in private subnets, you have several options: +#### Option 1: AWS Session Manager Port Forwarding ```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) +# Create an EC2 instance in the same VPC (if needed) and use Session Manager +aws ssm start-session --target --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["5432"],"localPortNumber":["5432"]}' +``` -# Connect with psql -psql "$CONNECTION_STRING" +#### Option 2: SSH Tunnel via Bastion Host +```bash +# If you have a bastion host in the public subnet +ssh -L 5432:$DB_HOST:5432 user@bastion-host +psql -h localhost -U $DB_USER -d $DB_NAME ``` ## Setting up the Database @@ -135,10 +179,19 @@ curl "$API_ENDPOINT" ``` The function will: -1. Connect to the PostgreSQL database -2. Query the `users` table -3. Log the results -4. Return "Done" +1. Connect to the PostgreSQL database using SSL/TLS with RDS root certificate verification +2. Query the `users` table with a 3-second timeout to prevent hanging +3. Log the results for each user found +4. Return a JSON array of `User` objects with `id` and `username` fields + +Example response: +```json +[ + {"id": 1, "username": "alice"}, + {"id": 2, "username": "bob"}, + {"id": 3, "username": "charlie"} +] +``` ## Monitoring @@ -150,14 +203,18 @@ 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: +โœ… **Security Best Practices Implemented**: + +This example follows AWS security best practices: + +1. **Private Database**: Database is deployed in private subnets with no internet access +2. **Network Segmentation**: Separate public and private subnets with proper routing +3. **Security Groups**: Restrictive security groups following least privilege principle +4. **Secrets Management**: Database credentials stored in AWS Secrets Manager +5. **Encryption**: SSL/TLS for database connections with certificate verification +6. **VPC Endpoints**: Administrative access through SSM VPC endpoints -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 +The infrastructure implements secure networking patterns suitable for production workloads. ## Cost Optimization @@ -177,21 +234,34 @@ To delete all resources: sam delete --stack-name servicelifecycle-stack ``` +## SSL Certificate Support + +This example includes RDS root certificates for secure SSL/TLS connections. Currently supported regions: +- `us-east-1`: US East (N. Virginia) +- `eu-central-1`: Europe (Frankfurt) + +To add support for additional regions: +1. Download the appropriate root certificate from [AWS RDS SSL documentation](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.SSL.html) +2. Create a new Swift file in `Sources/RDSCertificates/` with the certificate PEM data +3. Add the region mapping to `rootRDSCertificates` dictionary in `RootRDSCert.swift` + ## 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 +1. Check security groups allow traffic on port 5432 between Lambda and RDS security groups +2. Verify the Lambda function is deployed in subnets with proper routing to private subnets +3. Check VPC configuration and routing tables +4. Verify database credentials are correctly retrieved from Secrets Manager +5. Ensure the RDS instance is running and healthy ### Database connection timeout -The PostgreSQL client may freeze if the database is unreachable. This is a known issue with PostgresNIO. Ensure: +The PostgreSQL client may hang if the database is unreachable. This example implements a 3-second timeout mechanism to prevent this issue. If the connection or query takes longer than 3 seconds, the function will timeout and return an empty array. Ensure: 1. Database is running and accessible 2. Security groups are properly configured 3. Network connectivity is available +4. SSL certificates are properly configured for your AWS region ### Build failures @@ -199,12 +269,17 @@ Ensure you have: 1. Swift 6.x toolchain installed 2. Docker running 3. Proper network connectivity for downloading dependencies +4. All required dependencies: PostgresNIO, AWSLambdaRuntime, and ServiceLifecycle ## Files - `template.yaml`: SAM template defining all AWS resources +- `INFRASTRUCTURE.md`: Detailed infrastructure architecture documentation - `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 +- `Sources/Lambda.swift`: Swift Lambda function code with ServiceLifecycle integration +- `Sources/Timeout.swift`: Timeout utility to prevent database connection hangs +- `Sources/RDSCertificates/RootRDSCert.swift`: RDS root certificate management +- `Sources/RDSCertificates/us-east-1.swift`: US East 1 region root certificate +- `Sources/RDSCertificates/eu-central-1.swift`: EU Central 1 region root certificate +- `Package.swift`: Swift package definition with PostgresNIO, AWSLambdaRuntime, and ServiceLifecycle dependencies From 7ee17863197d6bd4f7922ef7e0a445e0a4c7b4cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 17:19:16 +0200 Subject: [PATCH 17/38] fix yaml lint --- Examples/ServiceLifecycle/template.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle/template.yaml index f79c3cbe..5b02d770 100644 --- a/Examples/ServiceLifecycle/template.yaml +++ b/Examples/ServiceLifecycle/template.yaml @@ -63,7 +63,7 @@ Resources: Tags: - Key: Name Value: ServiceLifecycle-Public-Subnet-2 - + # Private Subnet 1 for RDS PrivateSubnet1: Type: AWS::EC2::Subnet @@ -118,7 +118,7 @@ Resources: Properties: RouteTableId: !Ref PublicRouteTable SubnetId: !Ref PublicSubnet2 - + # NAT Gateway for private subnets NatGatewayEIP: Type: AWS::EC2::EIP @@ -217,7 +217,7 @@ Resources: Name: !Sub "${AWS::StackName}-db-credentials" Description: RDS database credentials GenerateSecretString: - SecretStringTemplate: '{"username":"postgres"}' + SecretStringTemplate: '{"username":"postgres"}' GenerateStringKey: "password" PasswordLength: 16 ExcludeCharacters: '"@/\\' @@ -232,7 +232,7 @@ Resources: ToPort: 5432 SourceSecurityGroupId: !Ref LambdaSecurityGroup Description: Allow PostgreSQL access from Lambda security group - + # Security Group for SSM VPC Endpoints SSMEndpointSecurityGroup: Type: AWS::EC2::SecurityGroup @@ -249,7 +249,7 @@ Resources: Tags: - Key: Name Value: ServiceLifecycle-SSM-Endpoint-SecurityGroup - + # Add ingress rule to Database Security Group for SSM connections DatabaseSecurityGroupIngressSSM: Type: AWS::EC2::SecurityGroupIngress @@ -334,7 +334,7 @@ Resources: Type: AWS::Serverless::Function Properties: CodeUri: ./LambdaWithServiceLifecycle.zip - #CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/LambdaWithServiceLifecycle/LambdaWithServiceLifecycle.zip + # CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/LambdaWithServiceLifecycle/LambdaWithServiceLifecycle.zip Timeout: 60 Handler: swift.bootstrap # ignored by the Swift runtime Runtime: provided.al2 @@ -406,7 +406,7 @@ Outputs: Port: ${PostgreSQLDatabase.Endpoint.Port} Database: ${DBName} Credentials: Use AWS Secrets Manager to retrieve username and password - + # SSM VPC Endpoints SSMEndpoints: Description: SSM VPC Endpoints for CloudShell access @@ -414,7 +414,7 @@ Outputs: SSM Endpoint: ${SSMEndpoint} SSM Messages Endpoint: ${SSMMessagesEndpoint} EC2 Messages Endpoint: ${EC2MessagesEndpoint} - + # CloudShell Connection Instructions CloudShellInstructions: Description: Instructions for connecting from CloudShell From 2ce51ac7f78aeba26f8a3245224d8e24182f9b55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 18:36:43 +0200 Subject: [PATCH 18/38] fix unused warnings --- Sources/AWSLambdaRuntime/Lambda+LocalServer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index 0dfcb456..76f37d60 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -51,7 +51,7 @@ extension Lambda { logger: Logger, _ body: sending @escaping () async throws -> Void ) async throws { - try await LambdaHTTPServer.withLocalServer( + _ = try await LambdaHTTPServer.withLocalServer( invocationEndpoint: invocationEndpoint, logger: logger ) { From c661cbe8e67426ce72b7c6e0ed8090b4c200237f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 22:26:54 +0200 Subject: [PATCH 19/38] change DB name --- Examples/ServiceLifecycle/localdb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ServiceLifecycle/localdb.sh b/Examples/ServiceLifecycle/localdb.sh index f091132d..838e8773 100644 --- a/Examples/ServiceLifecycle/localdb.sh +++ b/Examples/ServiceLifecycle/localdb.sh @@ -37,8 +37,8 @@ docker start postgres-db docker run -it --rm --network host \ -e PGPASSWORD=secret \ postgres:latest \ - psql -h localhost -U postgres -d test + psql -h localhost -U postgres -d servicelifecycle # Alternative: Connect using the postgres-db container itself -docker exec -it postgres-db psql -U postgres -d test +docker exec -it postgres-db psql -U postgres -d servicelifecycle From d4b5ebe54e1e84563876d2c4b0518fef7e8791f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 22:28:04 +0200 Subject: [PATCH 20/38] automatically populate the table at first usage --- .../ServiceLifecycle/Sources/Lambda.swift | 108 ++++++++++++------ 1 file changed, 76 insertions(+), 32 deletions(-) diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index ffe6b830..21a57d9f 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -25,72 +25,104 @@ struct User: Codable { @main struct LambdaFunction { - static func main() async throws { - try await LambdaFunction().main() - } - private let pgClient: PostgresClient - private var logger: Logger + private let logger: Logger + private init() throws { - self.logger = Logger(label: "ServiceLifecycleExample") + var logger = Logger(label: "ServiceLifecycleExample") + logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + self.logger = logger - self.pgClient = try LambdaFunction.preparePostgresClient( + 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") ?? "test", - logger: logger + 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 - // 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) + let runtime = LambdaRuntime(logger: self.logger, body: self.handler) /// Use ServiceLifecycle to manage the initialization and termination /// of the PGClient together with the LambdaRuntime let serviceGroup = ServiceGroup( - services: [pgClient, runtime], + services: [self.pgClient, runtime], gracefulShutdownSignals: [.sigterm], cancellationSignals: [.sigint], - logger: logger + 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: String, context: LambdaContext) async -> [User] { - // input event is ignored here - var result: [User] = [] 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 - // this is why there is a timeout, as suggested by - // https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 - logger.info("Connecting to the database") + // 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)) { - let rows = try await pgClient.query("SELECT id, username FROM users") - var users: [User] = [] - for try await (id, username) in rows.decode((Int, String).self) { - logger.info("Adding \(id) : \(username)") - users.append(User(id: id, username: username)) - } - return users + // check if table exists + try await prepareDatabase() + + // query users + return try await self.queryUsers() } } catch { - logger.error("PG Error: \(error)") + logger.error("Database Error", metadata: ["cause": "\(String(reflecting: error))"]) } + return result } - private static func preparePostgresClient( + /// 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 { + logger.trace("Preparing to the database") + do { + // initial creation of the table. This will fails if it already exists + try await self.pgClient.query(SQLStatements.createTable) + // it did not fail, it means the table is new and empty + 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 + } catch { + // propagate other errors + throw error + } + } + + /// Query the database + private func queryUsers() async throws -> [User] { + logger.trace("Querying to the database") + 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("Adding \(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, @@ -125,6 +157,18 @@ struct LambdaFunction { 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 { From e0bc6eadee97767d49c9e6fbf1eeb6ab634cad34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 22:34:02 +0200 Subject: [PATCH 21/38] improve logging --- Examples/ServiceLifecycle/Sources/Lambda.swift | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index 21a57d9f..6bb09914 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -76,9 +76,11 @@ struct LambdaFunction { // See: https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 result = try await timeout(deadline: .seconds(3)) { // check if table exists + logger.trace("Checking database") try await prepareDatabase() // query users + logger.trace("Querying database") return try await self.queryUsers() } } catch { @@ -94,13 +96,19 @@ struct LambdaFunction { private func prepareDatabase() async throws { logger.trace("Preparing to the database") 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 @@ -109,12 +117,11 @@ struct LambdaFunction { /// Query the database private func queryUsers() async throws -> [User] { - logger.trace("Querying to the database") 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("Adding \(id) : \(username)") + self.logger.trace("\(id) : \(username)") users.append(User(id: id, username: username)) } return users From 32b16bf602749cc113d988fb7e1976db54c649d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 22 Jul 2025 22:46:33 +0200 Subject: [PATCH 22/38] add comments --- Examples/ServiceLifecycle/Sources/Lambda.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index 6bb09914..855b5abb 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -76,6 +76,8 @@ struct LambdaFunction { // See: https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 result = try await timeout(deadline: .seconds(3)) { // check if table exists + // TODO: ideally, I want to do this once, after serviceGroup.run() is done + // but before the handler is called logger.trace("Checking database") try await prepareDatabase() @@ -94,7 +96,6 @@ struct LambdaFunction { /// 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 { - logger.trace("Preparing to the database") do { // initial creation of the table. This will fails if it already exists From 17992480514a5921f2cb6cc721b0c33c663e9b80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 23 Jul 2025 19:21:10 +0400 Subject: [PATCH 23/38] simplify infrastructure architecture --- Examples/ServiceLifecycle/INFRASTRUCTURE.md | 257 +++++++++----------- Examples/ServiceLifecycle/README.md | 65 ++--- Examples/ServiceLifecycle/deploy.sh | 2 +- Examples/ServiceLifecycle/template.yaml | 89 +------ 4 files changed, 131 insertions(+), 282 deletions(-) diff --git a/Examples/ServiceLifecycle/INFRASTRUCTURE.md b/Examples/ServiceLifecycle/INFRASTRUCTURE.md index 07e82988..d3efc3cb 100644 --- a/Examples/ServiceLifecycle/INFRASTRUCTURE.md +++ b/Examples/ServiceLifecycle/INFRASTRUCTURE.md @@ -1,154 +1,125 @@ -# Infrastructure Documentation - -This document describes the AWS infrastructure defined in `template.yaml` for the ServiceLifecycle Lambda with PostgreSQL example. - -## Architecture Overview - -The infrastructure consists of a secure VPC setup with public and private subnets, a Lambda function in public subnets, and an RDS PostgreSQL database in private subnets. The architecture follows AWS best practices for security and connectivity. - -```mermaid -graph TD - subgraph "AWS Cloud" - subgraph "VPC (10.0.0.0/16)" - subgraph "Public Subnets" - Lambda["Lambda Function"] - NAT["NAT Gateway"] - IGW["Internet Gateway"] - end - - subgraph "Private Subnets" - RDS["PostgreSQL RDS"] - SSM1["SSM Endpoint"] - SSM2["SSM Messages Endpoint"] - SSM3["EC2 Messages Endpoint"] - end - - Lambda -- "Egress to DB (5432)" --> RDS - Lambda -- "Egress to AWS APIs (443)" --> Internet - - RDS -- "Ingress from Lambda (5432)" --> Lambda - RDS -- "Ingress from SSM (5432)" --> SSM1 - - NAT -- "Outbound traffic" --> IGW - IGW -- "Internet Access" --> Internet - end - - SecretsManager["Secrets Manager"] - APIGateway["API Gateway"] - EC2["Amazon EC2"] - - APIGateway --> Lambda - Lambda --> SecretsManager - EC2 -- "Via SSM Endpoints" --> SSM1 - SSM1 --> RDS - end - - User["User"] --> APIGateway - Admin["Admin"] --> EC2 -``` - -## Key Components - -### Networking - -1. **VPC**: A dedicated VPC with CIDR block `10.0.0.0/16` -2. **Subnets**: - - Public Subnets (10.0.1.0/24, 10.0.2.0/24): For Lambda and NAT Gateway - - Private Subnets (10.0.3.0/24, 10.0.4.0/24): For RDS and VPC endpoints -3. **Internet Gateway**: Provides internet access for public subnets -4. **NAT Gateway**: Allows outbound internet access from private subnets -5. **Route Tables**: Separate route tables for public and private subnets - -### Security - -1. **Security Groups**: - - **Lambda Security Group**: Restricts outbound traffic to: - - PostgreSQL (5432) within the VPC - - HTTPS (443) to the internet - - **Database Security Group**: Allows inbound PostgreSQL connections only from: - - Lambda Security Group - - SSM Endpoint Security Group - - **SSM Endpoint Security Group**: Allows HTTPS inbound for SSM connections - -2. **Encryption**: - - RDS storage encryption enabled - - SSL/TLS for database connections with certificate verification - - Secrets Manager for secure credential storage - -### Compute & Database - -1. **Lambda Function**: - - Runtime: provided.al2 (Swift) - - Memory: 512MB - - Timeout: 60 seconds - - VPC integration with public subnets - -2. **RDS PostgreSQL**: - - Instance class: db.t3.micro - - Engine version: 15.7 - - Storage: 20GB gp2 - - Placed in private subnets - - Not publicly accessible - -### Access & Management - -1. **Secrets Manager**: - - Stores database credentials - - Auto-generates secure password - - Referenced by Lambda and RDS - -2. **VPC Endpoints**: - - SSM Endpoint - - SSM Messages Endpoint - - EC2 Messages Endpoint - - Enables CloudShell access to private resources - -3. **Function URL**: - - HTTP endpoint for invoking Lambda function +# Infrastructure Architecture + +This document describes the AWS infrastructure deployed by the ServiceLifecycle example's SAM template. + +## Overview + +The infrastructure consists of a secure VPC setup with public and private subnets, a PostgreSQL RDS instance in private subnets, and a Lambda function with VPC access. The architecture follows AWS best practices for security and network isolation. + +## Network Architecture + +### VPC Configuration +- **VPC**: Custom VPC with CIDR block `10.0.0.0/16` +- **DNS Support**: DNS hostnames and DNS resolution enabled + +### Subnet Layout +- **Public Subnets**: + - Public Subnet 1: `10.0.1.0/24` (AZ 1) + - Public Subnet 2: `10.0.2.0/24` (AZ 2) + - Used for Lambda functions and NAT Gateway + - Auto-assign public IP addresses enabled + +- **Private Subnets**: + - Private Subnet 1: `10.0.3.0/24` (AZ 1) + - Private Subnet 2: `10.0.4.0/24` (AZ 2) + - Used for RDS PostgreSQL database + - No public IP addresses assigned + +### Network Components +- **Internet Gateway**: Provides internet access for public subnets +- **NAT Gateway**: Deployed in Public Subnet 1, allows private subnets to access the internet +- **Route Tables**: + - Public Route Table: Routes traffic to the Internet Gateway + - Private Route Table: Routes traffic through the NAT Gateway + +## Security Groups + +### Lambda Security Group +- **Outbound Rules**: + - PostgreSQL (5432): Restricted to VPC CIDR `10.0.0.0/16` + - HTTPS (443): Open to `0.0.0.0/0` for AWS service access + +### Database Security Group +- **Inbound Rules**: + - PostgreSQL (5432): Only allows connections from the Lambda Security Group + +## Database Configuration + +### PostgreSQL RDS Instance +- **Instance Type**: `db.t3.micro` (cost-optimized) +- **Engine**: PostgreSQL 15.7 +- **Storage**: 20GB GP2 (SSD) +- **Network**: Deployed in private subnets with no public access +- **Security**: + - Storage encryption enabled + - SSL/TLS connections supported + - Credentials stored in AWS Secrets Manager +- **High Availability**: Multi-AZ disabled (development configuration) +- **Backup**: Automated backups disabled (development configuration) + +### Database Subnet Group +- Spans both private subnets for availability + +## Lambda Function Configuration + +### Service Lifecycle Lambda +- **Runtime**: Custom runtime (provided.al2) +- **Architecture**: ARM64 +- **Memory**: 512MB +- **Timeout**: 60 seconds +- **Network**: Deployed in public subnets with access to both internet and private resources +- **Environment Variables**: + - `LOG_LEVEL`: trace + - `DB_HOST`: RDS endpoint address + - `DB_USER`: Retrieved from Secrets Manager + - `DB_PASSWORD`: Retrieved from Secrets Manager + - `DB_NAME`: Database name from parameter + +## API Gateway + +- **Type**: HTTP API +- **Integration**: Direct Lambda integration +- **Authentication**: None (for demonstration purposes) + +## Secrets Management + +### Database Credentials +- **Storage**: AWS Secrets Manager +- **Secret Name**: `{StackName}-db-credentials` +- **Content**: + - Username: "postgres" + - Password: Auto-generated 16-character password + - Special characters excluded: `"@/\` + +## SAM Outputs + +The template provides several outputs to facilitate working with the deployed resources: + +- **APIGatewayEndpoint**: URL to invoke the Lambda function +- **DatabaseEndpoint**: Hostname for the PostgreSQL instance +- **DatabasePort**: Port number for PostgreSQL (5432) +- **DatabaseName**: Name of the created database +- **DatabaseSecretArn**: ARN of the secret containing credentials +- **DatabaseConnectionInstructions**: Instructions for retrieving connection details +- **ConnectionDetails**: Consolidated connection information ## Security Considerations -1. **Network Isolation**: Database is in private subnets, not directly accessible from the internet -2. **Least Privilege**: Security groups follow principle of least privilege -3. **Encryption**: Data at rest is encrypted -4. **Secure Credentials**: No hardcoded credentials, using Secrets Manager -5. **SSL/TLS**: Database connections use SSL/TLS with certificate verification +This infrastructure implements several security best practices: -## Accessing the Database - -The database is in a private subnet and not directly accessible from the internet. To connect: - -1. **From Lambda**: Direct connection through VPC networking -2. **From Amazon EC2**: Through SSM VPC endpoints -3. **From your local machine**: Options include: - - SSH tunnel through a bastion host - - AWS Session Manager port forwarding - - AWS Cloud9 environment in the same VPC - -## Deployment - -Deploy this infrastructure using AWS SAM: - -```bash -sam deploy -``` - -## Outputs - -After deployment, the following information is available in CloudFormation outputs: - -- API Gateway endpoint URL -- Database endpoint hostname and port -- Database name -- Secret ARN for retrieving credentials -- Connection instructions +1. **Network Isolation**: Database is placed in private subnets with no direct internet access +2. **Least Privilege**: Security groups restrict traffic to only necessary ports and sources +3. **Encryption**: Database storage is encrypted at rest +4. **Secure Credentials**: Database credentials are managed through AWS Secrets Manager +5. **Secure Communication**: Lambda function connects to database over encrypted connections ## Cost Optimization -The template uses cost-effective resources: -- db.t3.micro RDS instance (eligible for free tier) +The template uses cost-effective resources suitable for development: + +- `db.t3.micro` instance (eligible for free tier) - Minimal storage allocation (20GB) -- Single-AZ deployment +- No Multi-AZ deployment - No automated backups For production workloads, consider adjusting these settings based on your requirements. \ No newline at end of file diff --git a/Examples/ServiceLifecycle/README.md b/Examples/ServiceLifecycle/README.md index 6a9242b0..3d8d66b6 100644 --- a/Examples/ServiceLifecycle/README.md +++ b/Examples/ServiceLifecycle/README.md @@ -6,11 +6,10 @@ This example demonstrates a Swift Lambda function that uses ServiceLifecycle to - **Swift Lambda Function**: Uses ServiceLifecycle to manage PostgreSQL client lifecycle, deployed in public subnets - **PostgreSQL RDS**: Database instance in private subnets with SSL/TLS encryption -- **Function URL**: HTTP endpoint to invoke the Lambda function +- **HTTP API Gateway**: HTTP endpoint to invoke the Lambda function - **VPC**: Custom VPC with public subnets for Lambda/NAT Gateway and private subnets for RDS - **Security**: SSL/TLS connections with RDS root certificate verification, secure networking with security groups - **Timeout Handling**: 3-second timeout mechanism to prevent database connection hangs -- **VPC Endpoints**: SSM endpoints for administrative access to private resources - **Secrets Manager**: Secure credential storage and management For detailed infrastructure information, see `INFRASTRUCTURE.md`. @@ -38,6 +37,8 @@ The Lambda function demonstrates several key concepts: ## Database Schema +In the context of this demo, the Lambda function creates the table and populates it with data at first run. + The Lambda function expects a `users` table with the following structure and returns results as `User` objects: ```sql @@ -89,7 +90,7 @@ The Lambda function uses the following environment variables for database connec ## Getting Connection Details -After deployment, get the database connection details: +After deployment, get the database and API Gateway connection details: ```bash aws cloudformation describe-stacks \ @@ -107,63 +108,25 @@ The output will include: ## Connecting to the Database -### Important: Database Access - -The PostgreSQL database is deployed in **private subnets** and is **not directly accessible** from the internet. This follows AWS security best practices. +The database is deployed in **private subnets** and is **not directly accessible** from the internet. This follows AWS security best practices. -### From Amazon EC2 (Recommended for testing) +You may create an Amazon EC2 instance (virtual machine) in the public subnet of the VPC and use it as a jump host to connect to the database. The SAM template doesn't create this for you. [This is left as an exercise to the reader](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/LaunchingAndUsingInstances.html). -an Amazon EC2 instance deployed in the publci subnet of the VPC can connect through the VPC endpoints configured in the template: +You can access the database connection details in the output of the SAM template: ```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_PORT=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabasePort`].OutputValue' --output text) DB_NAME=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseName`].OutputValue' --output text) # Get the database password from Secrets Manager SECRET_ARN=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`DatabaseSecretArn`].OutputValue' --output text) +DB_USERNAME=$(aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" --query 'SecretString' --output text | jq -r '.username') DB_PASSWORD=$(aws secretsmanager get-secret-value --secret-id "$SECRET_ARN" --query 'SecretString' --output text | jq -r '.password') # Connect with psql on Amazon EC2 -psql -h $DB_HOST -U $DB_USER -d $DB_NAME -``` - -### From your local machine - -Since the database is in private subnets, you have several options: - -#### Option 1: AWS Session Manager Port Forwarding -```bash -# Create an EC2 instance in the same VPC (if needed) and use Session Manager -aws ssm start-session --target --document-name AWS-StartPortForwardingSession --parameters '{"portNumber":["5432"],"localPortNumber":["5432"]}' -``` - -#### Option 2: SSH Tunnel via Bastion Host -```bash -# If you have a bastion host in the public subnet -ssh -L 5432:$DB_HOST:5432 user@bastion-host -psql -h localhost -U $DB_USER -d $DB_NAME -``` - -## 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'); +psql -h "$DB_HOST:$DB_PORT" -U "$DB_USER" -d "$DB_NAME" ``` ## Testing the Lambda Function @@ -175,7 +138,7 @@ Get the API Gateway endpoint and test the function: 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" +curl -X POST -d '"empty string - input is not used"' "$API_ENDPOINT" ``` The function will: @@ -247,17 +210,19 @@ To add support for additional regions: ## Troubleshooting +when deploying with SAM and the `template.yaml` file included in this example, there shouldn't be any error. However, when you try to create such infarstructure on your own or using different infrastructure as code (IaC) tools, it's likely to iterate before getting everything configured. We compiled a couple of the most common configuration errors and their solution: + ### Lambda can't connect to database 1. Check security groups allow traffic on port 5432 between Lambda and RDS security groups 2. Verify the Lambda function is deployed in subnets with proper routing to private subnets 3. Check VPC configuration and routing tables -4. Verify database credentials are correctly retrieved from Secrets Manager +4. Verify database credentials are correctly retrieved from Secrets Manager and that the Lambda execution policies have permissions to read the secret. 5. Ensure the RDS instance is running and healthy ### Database connection timeout -The PostgreSQL client may hang if the database is unreachable. This example implements a 3-second timeout mechanism to prevent this issue. If the connection or query takes longer than 3 seconds, the function will timeout and return an empty array. Ensure: +The PostgreSQL client may freeze if the database is unreachable. This example implements a 3-second timeout mechanism to prevent this issue. If the connection or query takes longer than 3 seconds, the function will timeout and return an empty array. Ensure: 1. Database is running and accessible 2. Security groups are properly configured 3. Network connectivity is available diff --git a/Examples/ServiceLifecycle/deploy.sh b/Examples/ServiceLifecycle/deploy.sh index 02a33452..f1138e84 100755 --- a/Examples/ServiceLifecycle/deploy.sh +++ b/Examples/ServiceLifecycle/deploy.sh @@ -32,4 +32,4 @@ 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)" +echo "curl \$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text)" diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle/template.yaml index 5b02d770..ba90a161 100644 --- a/Examples/ServiceLifecycle/template.yaml +++ b/Examples/ServiceLifecycle/template.yaml @@ -233,76 +233,6 @@ Resources: SourceSecurityGroupId: !Ref LambdaSecurityGroup Description: Allow PostgreSQL access from Lambda security group - # Security Group for SSM VPC Endpoints - SSMEndpointSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupName: ServiceLifecycle-SSM-Endpoint-SG - GroupDescription: Security group for SSM VPC endpoints - VpcId: !Ref VPC - SecurityGroupIngress: - - IpProtocol: tcp - FromPort: 443 - ToPort: 443 - CidrIp: 0.0.0.0/0 - Description: Allow HTTPS inbound for SSM connections - Tags: - - Key: Name - Value: ServiceLifecycle-SSM-Endpoint-SecurityGroup - - # Add ingress rule to Database Security Group for SSM connections - DatabaseSecurityGroupIngressSSM: - Type: AWS::EC2::SecurityGroupIngress - Properties: - GroupId: !Ref DatabaseSecurityGroup - IpProtocol: tcp - FromPort: 5432 - ToPort: 5432 - SourceSecurityGroupId: !Ref SSMEndpointSecurityGroup - Description: Allow PostgreSQL access from SSM endpoints - - # SSM VPC Endpoint - SSMEndpoint: - Type: AWS::EC2::VPCEndpoint - Properties: - VpcEndpointType: Interface - PrivateDnsEnabled: true - ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssm" - VpcId: !Ref VPC - SubnetIds: - - !Ref PrivateSubnet1 - - !Ref PrivateSubnet2 - SecurityGroupIds: - - !Ref SSMEndpointSecurityGroup - - # SSM Messages VPC Endpoint - SSMMessagesEndpoint: - Type: AWS::EC2::VPCEndpoint - Properties: - VpcEndpointType: Interface - PrivateDnsEnabled: true - ServiceName: !Sub "com.amazonaws.${AWS::Region}.ssmmessages" - VpcId: !Ref VPC - SubnetIds: - - !Ref PrivateSubnet1 - - !Ref PrivateSubnet2 - SecurityGroupIds: - - !Ref SSMEndpointSecurityGroup - - # EC2 Messages VPC Endpoint - EC2MessagesEndpoint: - Type: AWS::EC2::VPCEndpoint - Properties: - VpcEndpointType: Interface - PrivateDnsEnabled: true - ServiceName: !Sub "com.amazonaws.${AWS::Region}.ec2messages" - VpcId: !Ref VPC - SubnetIds: - - !Ref PrivateSubnet1 - - !Ref PrivateSubnet2 - SecurityGroupIds: - - !Ref SSMEndpointSecurityGroup - # PostgreSQL RDS Instance PostgreSQLDatabase: Type: AWS::RDS::DBInstance @@ -349,7 +279,7 @@ Resources: - !Ref PublicSubnet2 Environment: Variables: - LOG_LEVEL: debug + LOG_LEVEL: trace DB_HOST: !GetAtt PostgreSQLDatabase.Endpoint.Address DB_USER: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:username}}']] DB_PASSWORD: !Join ['', ['{{resolve:secretsmanager:', !Ref DatabaseSecret, ':SecretString:password}}']] @@ -406,20 +336,3 @@ Outputs: Port: ${PostgreSQLDatabase.Endpoint.Port} Database: ${DBName} Credentials: Use AWS Secrets Manager to retrieve username and password - - # SSM VPC Endpoints - SSMEndpoints: - Description: SSM VPC Endpoints for CloudShell access - Value: !Sub | - SSM Endpoint: ${SSMEndpoint} - SSM Messages Endpoint: ${SSMMessagesEndpoint} - EC2 Messages Endpoint: ${EC2MessagesEndpoint} - - # CloudShell Connection Instructions - CloudShellInstructions: - Description: Instructions for connecting from CloudShell - Value: !Sub | - 1. Open AWS CloudShell - 2. Run: aws secretsmanager get-secret-value --secret-id ${DatabaseSecret} --query 'SecretString' --output text - 3. Parse the JSON output to get username and password - 4. Connect using: PGPASSWORD= psql -h ${PostgreSQLDatabase.Endpoint.Address} -U -d ${DBName} From 018d9cef4b2dfab8029cda4158c7c8ce2aad6703 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 23 Jul 2025 19:40:51 +0400 Subject: [PATCH 24/38] add api gateway event as input and output --- Examples/ServiceLifecycle/Package.swift | 4 +++- Examples/ServiceLifecycle/README.md | 2 +- Examples/ServiceLifecycle/Sources/Lambda.swift | 9 +++++++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/Examples/ServiceLifecycle/Package.swift b/Examples/ServiceLifecycle/Package.swift index 0c3ee2b9..a3d85298 100644 --- a/Examples/ServiceLifecycle/Package.swift +++ b/Examples/ServiceLifecycle/Package.swift @@ -14,6 +14,7 @@ let package = Package( dependencies: [ .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-aws-lambda-events.git", from: "1.0.0"), .package(url: "https://github.com/swift-server/swift-service-lifecycle.git", from: "2.6.3"), ], targets: [ @@ -21,8 +22,9 @@ let package = Package( name: "LambdaWithServiceLifecycle", dependencies: [ .product(name: "PostgresNIO", package: "postgres-nio"), - .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), ] ) ] diff --git a/Examples/ServiceLifecycle/README.md b/Examples/ServiceLifecycle/README.md index 3d8d66b6..7f1bb1f5 100644 --- a/Examples/ServiceLifecycle/README.md +++ b/Examples/ServiceLifecycle/README.md @@ -138,7 +138,7 @@ Get the API Gateway endpoint and test the function: API_ENDPOINT=$(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text) # Test the function -curl -X POST -d '"empty string - input is not used"' "$API_ENDPOINT" +curl "$API_ENDPOINT" ``` The function will: diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index 855b5abb..558976c8 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -13,6 +13,7 @@ //===----------------------------------------------------------------------===// import AWSLambdaRuntime +import AWSLambdaEvents import Logging import PostgresNIO import ServiceLifecycle @@ -66,7 +67,7 @@ struct LambdaFunction { /// Function handler. This code is called at each function invocation /// input event is ignored in this demo. - private func handler(event: String, context: LambdaContext) async -> [User] { + private func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { var result: [User] = [] do { @@ -89,7 +90,11 @@ struct LambdaFunction { logger.error("Database Error", metadata: ["cause": "\(String(reflecting: error))"]) } - return result + return try .init( + statusCode: .ok, + headers: ["content-type": "application/json"], + encodableBody: result + ) } /// Prepare the database From 66ac85004362e8af4ef8b53f82c7b4417d9d101c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 23 Jul 2025 19:54:19 +0400 Subject: [PATCH 25/38] add license header --- Examples/ServiceLifecycle/localdb.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/ServiceLifecycle/localdb.sh b/Examples/ServiceLifecycle/localdb.sh index 838e8773..db615b4d 100644 --- a/Examples/ServiceLifecycle/localdb.sh +++ b/Examples/ServiceLifecycle/localdb.sh @@ -1,3 +1,4 @@ +#!/bin/bash ##===----------------------------------------------------------------------===## ## ## This source file is part of the SwiftAWSLambdaRuntime open source project From add035c558c3dc5cb62d5e6d7530510a6fd21bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 23 Jul 2025 19:56:04 +0400 Subject: [PATCH 26/38] remove the word hang --- Examples/ServiceLifecycle/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Examples/ServiceLifecycle/README.md b/Examples/ServiceLifecycle/README.md index 7f1bb1f5..693cf68f 100644 --- a/Examples/ServiceLifecycle/README.md +++ b/Examples/ServiceLifecycle/README.md @@ -9,7 +9,7 @@ This example demonstrates a Swift Lambda function that uses ServiceLifecycle to - **HTTP API Gateway**: HTTP endpoint to invoke the Lambda function - **VPC**: Custom VPC with public subnets for Lambda/NAT Gateway and private subnets for RDS - **Security**: SSL/TLS connections with RDS root certificate verification, secure networking with security groups -- **Timeout Handling**: 3-second timeout mechanism to prevent database connection hangs +- **Timeout Handling**: 3-second timeout mechanism to prevent database connection freeze - **Secrets Manager**: Secure credential storage and management For detailed infrastructure information, see `INFRASTRUCTURE.md`. @@ -22,7 +22,7 @@ The Lambda function demonstrates several key concepts: 2. **SSL/TLS Security**: Connections to RDS use SSL/TLS with full certificate verification using region-specific RDS root certificates. -3. **Timeout Protection**: A custom timeout mechanism prevents the function from hanging when the database is unreachable (addresses PostgresNIO issue #489). +3. **Timeout Protection**: A custom timeout mechanism prevents the function from freezing when the database is unreachable (addresses PostgresNIO issue #489). 4. **Structured Response**: Returns a JSON array of `User` objects, making it suitable for API integration. @@ -143,7 +143,7 @@ curl "$API_ENDPOINT" The function will: 1. Connect to the PostgreSQL database using SSL/TLS with RDS root certificate verification -2. Query the `users` table with a 3-second timeout to prevent hanging +2. Query the `users` table with a 3-second timeout to prevent freezing 3. Log the results for each user found 4. Return a JSON array of `User` objects with `id` and `username` fields @@ -243,7 +243,7 @@ Ensure you have: - `samconfig.toml`: SAM configuration file - `deploy.sh`: Deployment script - `Sources/Lambda.swift`: Swift Lambda function code with ServiceLifecycle integration -- `Sources/Timeout.swift`: Timeout utility to prevent database connection hangs +- `Sources/Timeout.swift`: Timeout utility to prevent database connection freezes - `Sources/RDSCertificates/RootRDSCert.swift`: RDS root certificate management - `Sources/RDSCertificates/us-east-1.swift`: US East 1 region root certificate - `Sources/RDSCertificates/eu-central-1.swift`: EU Central 1 region root certificate From 86ddb9ec1c6a32270e273ee2bf5cf51940946e57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 23 Jul 2025 19:56:35 +0400 Subject: [PATCH 27/38] swift format --- Examples/ServiceLifecycle/Sources/Lambda.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle/Sources/Lambda.swift index 558976c8..0b3e6c40 100644 --- a/Examples/ServiceLifecycle/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle/Sources/Lambda.swift @@ -12,8 +12,8 @@ // //===----------------------------------------------------------------------===// -import AWSLambdaRuntime import AWSLambdaEvents +import AWSLambdaRuntime import Logging import PostgresNIO import ServiceLifecycle @@ -77,7 +77,7 @@ struct LambdaFunction { // See: https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 result = try await timeout(deadline: .seconds(3)) { // check if table exists - // TODO: ideally, I want to do this once, after serviceGroup.run() is done + // TODO: ideally, I want to do this once, after serviceGroup.run() is done // but before the handler is called logger.trace("Checking database") try await prepareDatabase() From de6c5ad5b7eede84fa71ec65d0bc2f718f7b9e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Wed, 23 Jul 2025 20:17:43 +0400 Subject: [PATCH 28/38] disable incorrect shellcheck warning --- Examples/ServiceLifecycle/deploy.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Examples/ServiceLifecycle/deploy.sh b/Examples/ServiceLifecycle/deploy.sh index f1138e84..c464e07b 100755 --- a/Examples/ServiceLifecycle/deploy.sh +++ b/Examples/ServiceLifecycle/deploy.sh @@ -32,4 +32,5 @@ 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)" +# shellcheck disable=SC2006 +echo "curl $(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text)" From 3f35fb1b37a42d9626815ce4627a6457090b0422 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Thu, 24 Jul 2025 15:02:27 +0400 Subject: [PATCH 29/38] another attemp to fix shelcheck --- Examples/ServiceLifecycle/deploy.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ServiceLifecycle/deploy.sh b/Examples/ServiceLifecycle/deploy.sh index c464e07b..262cbf65 100755 --- a/Examples/ServiceLifecycle/deploy.sh +++ b/Examples/ServiceLifecycle/deploy.sh @@ -32,5 +32,5 @@ 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:" -# shellcheck disable=SC2006 +# shellcheck disable=SC2006,SC2016 echo "curl $(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text)" From a8bbe45ce04887b863c66a3895eff7579c281bdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 25 Jul 2025 21:15:28 +0400 Subject: [PATCH 30/38] rename project + remove public VPC and NAT --- .../.gitignore | 0 .../INFRASTRUCTURE.md | 88 ++++++++---- .../Package.swift | 0 .../README.md | 33 +++-- .../Sources/Lambda.swift | 0 .../Sources/RDSCertificates/RootRDSCert.swift | 0 .../RDSCertificates/eu-central-1.swift | 0 .../Sources/RDSCertificates/us-east-1.swift | 0 .../Sources/Timeout.swift | 0 .../deploy.sh | 0 .../localdb.sh | 0 .../samconfig.toml | 0 .../template.yaml | 128 +----------------- 13 files changed, 83 insertions(+), 166 deletions(-) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/.gitignore (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/INFRASTRUCTURE.md (56%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/Package.swift (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/README.md (84%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/Sources/Lambda.swift (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/Sources/RDSCertificates/RootRDSCert.swift (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/Sources/RDSCertificates/eu-central-1.swift (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/Sources/RDSCertificates/us-east-1.swift (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/Sources/Timeout.swift (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/deploy.sh (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/localdb.sh (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/samconfig.toml (100%) rename Examples/{ServiceLifecycle => ServiceLifecycle+Postgres}/template.yaml (65%) diff --git a/Examples/ServiceLifecycle/.gitignore b/Examples/ServiceLifecycle+Postgres/.gitignore similarity index 100% rename from Examples/ServiceLifecycle/.gitignore rename to Examples/ServiceLifecycle+Postgres/.gitignore diff --git a/Examples/ServiceLifecycle/INFRASTRUCTURE.md b/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md similarity index 56% rename from Examples/ServiceLifecycle/INFRASTRUCTURE.md rename to Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md index d3efc3cb..890c7fd2 100644 --- a/Examples/ServiceLifecycle/INFRASTRUCTURE.md +++ b/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md @@ -4,7 +4,7 @@ This document describes the AWS infrastructure deployed by the ServiceLifecycle ## Overview -The infrastructure consists of a secure VPC setup with public and private subnets, a PostgreSQL RDS instance in private subnets, and a Lambda function with VPC access. The architecture follows AWS best practices for security and network isolation. +The infrastructure consists of a secure VPC setup with private subnets only, containing both the PostgreSQL RDS instance and Lambda function. The architecture is optimized for cost and security with complete network isolation. ## Network Architecture @@ -13,31 +13,22 @@ The infrastructure consists of a secure VPC setup with public and private subnet - **DNS Support**: DNS hostnames and DNS resolution enabled ### Subnet Layout -- **Public Subnets**: - - Public Subnet 1: `10.0.1.0/24` (AZ 1) - - Public Subnet 2: `10.0.2.0/24` (AZ 2) - - Used for Lambda functions and NAT Gateway - - Auto-assign public IP addresses enabled - - **Private Subnets**: - Private Subnet 1: `10.0.3.0/24` (AZ 1) - Private Subnet 2: `10.0.4.0/24` (AZ 2) - - Used for RDS PostgreSQL database + - Used for RDS PostgreSQL database and Lambda function - No public IP addresses assigned + - Complete isolation from internet ### Network Components -- **Internet Gateway**: Provides internet access for public subnets -- **NAT Gateway**: Deployed in Public Subnet 1, allows private subnets to access the internet -- **Route Tables**: - - Public Route Table: Routes traffic to the Internet Gateway - - Private Route Table: Routes traffic through the NAT Gateway +- **VPC-only architecture**: No internet connectivity required +- **Route Tables**: Default VPC routing for internal communication ## Security Groups ### Lambda Security Group - **Outbound Rules**: - PostgreSQL (5432): Restricted to VPC CIDR `10.0.0.0/16` - - HTTPS (443): Open to `0.0.0.0/0` for AWS service access ### Database Security Group - **Inbound Rules**: @@ -67,7 +58,7 @@ The infrastructure consists of a secure VPC setup with public and private subnet - **Architecture**: ARM64 - **Memory**: 512MB - **Timeout**: 60 seconds -- **Network**: Deployed in public subnets with access to both internet and private resources +- **Network**: Deployed in private subnets with access to database within VPC - **Environment Variables**: - `LOG_LEVEL`: trace - `DB_HOST`: RDS endpoint address @@ -107,19 +98,64 @@ The template provides several outputs to facilitate working with the deployed re This infrastructure implements several security best practices: -1. **Network Isolation**: Database is placed in private subnets with no direct internet access +1. **Complete Network Isolation**: Both database and Lambda are in private subnets with no direct acces to or from the internet 2. **Least Privilege**: Security groups restrict traffic to only necessary ports and sources 3. **Encryption**: Database storage is encrypted at rest 4. **Secure Credentials**: Database credentials are managed through AWS Secrets Manager 5. **Secure Communication**: Lambda function connects to database over encrypted connections -## Cost Optimization - -The template uses cost-effective resources suitable for development: - -- `db.t3.micro` instance (eligible for free tier) -- Minimal storage allocation (20GB) -- No Multi-AZ deployment -- No automated backups - -For production workloads, consider adjusting these settings based on your requirements. \ No newline at end of file +## Cost Analysis + +### Monthly Cost Breakdown (US East 1 Region) + +#### Billable AWS Resources: + +**1. RDS PostgreSQL Database** +- Instance (db.t3.micro): $13.87/month (730 hours ร— $0.019/hour) +- Storage (20GB GP2): $2.30/month (20GB ร— $0.115/GB/month) +- Backup Storage: $0 (BackupRetentionPeriod: 0) +- Multi-AZ: $0 (disabled) +- **RDS Subtotal: $16.17/month** + +**2. AWS Secrets Manager** +- Secret Storage: $0.40/month per secret +- API Calls: ~$0.05 per 10,000 calls (minimal for Lambda access) +- **Secrets Manager Subtotal: ~$0.45/month** + +**3. AWS Lambda** +- Memory: 512MB ARM64 +- Free Tier: 1M requests + 400,000 GB-seconds/month +- Development Usage: $0 (within free tier) +- **Lambda Subtotal: $0/month** + +**4. API Gateway (HTTP API)** +- Free Tier: 1M requests/month +- Development Usage: $0 (within free tier) +- **API Gateway Subtotal: $0/month** + +#### Free AWS Resources: +- VPC, Private Subnets, Security Groups, DB Subnet Group: $0 + +### Total Monthly Cost: + +| Service | Cost | Notes | +|---------|------|---------| +| RDS PostgreSQL | $16.17 | db.t3.micro + 20GB storage | +| Secrets Manager | $0.45 | 1 secret + minimal API calls | +| Lambda | $0.00 | Within free tier | +| API Gateway | $0.00 | Within free tier | +| VPC Components | $0.00 | No charges | +| **TOTAL** | **$16.62/month** | | + +### With RDS Free Tier (First 12 Months): +- RDS Instance: $0 (750 hours/month free) +- RDS Storage: $0 (20GB free) +- **Total with Free Tier: ~$0.45/month** + +### Production Scaling Estimates: +- Higher Lambda usage: +$0.20 per million requests +- More RDS storage: +$0.115 per additional GB/month +- Multi-AZ RDS: ~2x RDS instance cost +- Backup storage: $0.095/GB/month + +This architecture provides maximum cost efficiency while maintaining security and functionality for development workloads. \ No newline at end of file diff --git a/Examples/ServiceLifecycle/Package.swift b/Examples/ServiceLifecycle+Postgres/Package.swift similarity index 100% rename from Examples/ServiceLifecycle/Package.swift rename to Examples/ServiceLifecycle+Postgres/Package.swift diff --git a/Examples/ServiceLifecycle/README.md b/Examples/ServiceLifecycle+Postgres/README.md similarity index 84% rename from Examples/ServiceLifecycle/README.md rename to Examples/ServiceLifecycle+Postgres/README.md index 693cf68f..90565bc0 100644 --- a/Examples/ServiceLifecycle/README.md +++ b/Examples/ServiceLifecycle+Postgres/README.md @@ -1,18 +1,18 @@ -# ServiceLifecycle Lambda with PostgreSQL +# A swift Service Lifecycle Lambda function with a managed PostgreSQL database -This example demonstrates a Swift Lambda function that uses ServiceLifecycle to manage a PostgreSQL connection. The function connects to an RDS PostgreSQL database in private subnets and queries user data. +This example demonstrates a Swift Lambda function that uses Swift Service Lifecycle to manage a PostgreSQL connection. The function connects to an RDS PostgreSQL database in private subnets and queries user data. ## Architecture -- **Swift Lambda Function**: Uses ServiceLifecycle to manage PostgreSQL client lifecycle, deployed in public subnets -- **PostgreSQL RDS**: Database instance in private subnets with SSL/TLS encryption +- **Swift Lambda Function**: A network isolated Lambda function that Uses Swift ServiceLifecycle to manage PostgreSQL client lifecycle +- **PostgreSQL on Amazon RDS**: Database instance in private subnets with SSL/TLS encryption - **HTTP API Gateway**: HTTP endpoint to invoke the Lambda function -- **VPC**: Custom VPC with public subnets for Lambda/NAT Gateway and private subnets for RDS +- **VPC**: Custom VPC with private subnets only for complete network isolation - **Security**: SSL/TLS connections with RDS root certificate verification, secure networking with security groups - **Timeout Handling**: 3-second timeout mechanism to prevent database connection freeze - **Secrets Manager**: Secure credential storage and management -For detailed infrastructure information, see `INFRASTRUCTURE.md`. +For detailed infrastructure and cost information, see `INFRASTRUCTURE.md`. ## Implementation Details @@ -110,7 +110,7 @@ The output will include: The database is deployed in **private subnets** and is **not directly accessible** from the internet. This follows AWS security best practices. -You may create an Amazon EC2 instance (virtual machine) in the public subnet of the VPC and use it as a jump host to connect to the database. The SAM template doesn't create this for you. [This is left as an exercise to the reader](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/LaunchingAndUsingInstances.html). +To connect to the database, you would need to create an Amazon EC2 instance in a public subnet (which you'd need to add to the VPC) or use AWS Systems Manager Session Manager for secure access to an EC2 instance in a private subnet. The current template uses a private-only architecture for maximum security. You can access the database connection details in the output of the SAM template: @@ -171,23 +171,27 @@ sam logs -n ServiceLifecycleLambda --stack-name servicelifecycle-stack --tail This example follows AWS security best practices: 1. **Private Database**: Database is deployed in private subnets with no internet access -2. **Network Segmentation**: Separate public and private subnets with proper routing +2. **Complete Network Isolation**: Private subnets only with no internet connectivity 3. **Security Groups**: Restrictive security groups following least privilege principle 4. **Secrets Management**: Database credentials stored in AWS Secrets Manager 5. **Encryption**: SSL/TLS for database connections with certificate verification -6. **VPC Endpoints**: Administrative access through SSM VPC endpoints +6. **Minimal Attack Surface**: No public subnets or internet gateways The infrastructure implements secure networking patterns suitable for production workloads. ## Cost Optimization -The template uses: +The template is optimized for cost: - `db.t3.micro` instance (eligible for free tier) - Minimal storage allocation (20GB) - No Multi-AZ deployment - No automated backups +- No NAT Gateway or Internet Gateway +- Private-only architecture -For production workloads, adjust these settings based on your requirements. +**Estimated cost: ~$16.62/month (or ~$0.45/month with RDS Free Tier)** + +For detailed cost breakdown, see `INFRASTRUCTURE.md`. ## Cleanup @@ -215,10 +219,9 @@ when deploying with SAM and the `template.yaml` file included in this example, t ### Lambda can't connect to database 1. Check security groups allow traffic on port 5432 between Lambda and RDS security groups -2. Verify the Lambda function is deployed in subnets with proper routing to private subnets -3. Check VPC configuration and routing tables -4. Verify database credentials are correctly retrieved from Secrets Manager and that the Lambda execution policies have permissions to read the secret. -5. Ensure the RDS instance is running and healthy +2. Verify both Lambda and RDS are deployed in the same private subnets +3. Verify database credentials are correctly retrieved from Secrets Manager and that the Lambda execution policies have permissions to read the secret +4. Ensure the RDS instance is running and healthy ### Database connection timeout diff --git a/Examples/ServiceLifecycle/Sources/Lambda.swift b/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift similarity index 100% rename from Examples/ServiceLifecycle/Sources/Lambda.swift rename to Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift diff --git a/Examples/ServiceLifecycle/Sources/RDSCertificates/RootRDSCert.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/RootRDSCert.swift similarity index 100% rename from Examples/ServiceLifecycle/Sources/RDSCertificates/RootRDSCert.swift rename to Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/RootRDSCert.swift diff --git a/Examples/ServiceLifecycle/Sources/RDSCertificates/eu-central-1.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/eu-central-1.swift similarity index 100% rename from Examples/ServiceLifecycle/Sources/RDSCertificates/eu-central-1.swift rename to Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/eu-central-1.swift diff --git a/Examples/ServiceLifecycle/Sources/RDSCertificates/us-east-1.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/us-east-1.swift similarity index 100% rename from Examples/ServiceLifecycle/Sources/RDSCertificates/us-east-1.swift rename to Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/us-east-1.swift diff --git a/Examples/ServiceLifecycle/Sources/Timeout.swift b/Examples/ServiceLifecycle+Postgres/Sources/Timeout.swift similarity index 100% rename from Examples/ServiceLifecycle/Sources/Timeout.swift rename to Examples/ServiceLifecycle+Postgres/Sources/Timeout.swift diff --git a/Examples/ServiceLifecycle/deploy.sh b/Examples/ServiceLifecycle+Postgres/deploy.sh similarity index 100% rename from Examples/ServiceLifecycle/deploy.sh rename to Examples/ServiceLifecycle+Postgres/deploy.sh diff --git a/Examples/ServiceLifecycle/localdb.sh b/Examples/ServiceLifecycle+Postgres/localdb.sh similarity index 100% rename from Examples/ServiceLifecycle/localdb.sh rename to Examples/ServiceLifecycle+Postgres/localdb.sh diff --git a/Examples/ServiceLifecycle/samconfig.toml b/Examples/ServiceLifecycle+Postgres/samconfig.toml similarity index 100% rename from Examples/ServiceLifecycle/samconfig.toml rename to Examples/ServiceLifecycle+Postgres/samconfig.toml diff --git a/Examples/ServiceLifecycle/template.yaml b/Examples/ServiceLifecycle+Postgres/template.yaml similarity index 65% rename from Examples/ServiceLifecycle/template.yaml rename to Examples/ServiceLifecycle+Postgres/template.yaml index ba90a161..a9490a33 100644 --- a/Examples/ServiceLifecycle/template.yaml +++ b/Examples/ServiceLifecycle+Postgres/template.yaml @@ -25,45 +25,6 @@ Resources: - 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 for Lambda - 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 for Lambda - 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 - # Private Subnet 1 for RDS PrivateSubnet1: Type: AWS::EC2::Subnet @@ -88,83 +49,6 @@ Resources: - Key: Name Value: ServiceLifecycle-Private-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 - - # NAT Gateway for private subnets - NatGatewayEIP: - Type: AWS::EC2::EIP - DependsOn: InternetGatewayAttachment - Properties: - Domain: vpc - - NatGateway: - Type: AWS::EC2::NatGateway - Properties: - AllocationId: !GetAtt NatGatewayEIP.AllocationId - SubnetId: !Ref PublicSubnet1 - Tags: - - Key: Name - Value: ServiceLifecycle-NAT-Gateway - - # Route Table for private subnets - PrivateRouteTable: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref VPC - Tags: - - Key: Name - Value: ServiceLifecycle-Private-Routes - - # Route to NAT Gateway - DefaultPrivateRoute: - Type: AWS::EC2::Route - Properties: - RouteTableId: !Ref PrivateRouteTable - DestinationCidrBlock: 0.0.0.0/0 - NatGatewayId: !Ref NatGateway - - # Associate private subnets with route table - PrivateSubnet1RouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - RouteTableId: !Ref PrivateRouteTable - SubnetId: !Ref PrivateSubnet1 - - PrivateSubnet2RouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - RouteTableId: !Ref PrivateRouteTable - SubnetId: !Ref PrivateSubnet2 - # Security Group for RDS DatabaseSecurityGroup: Type: AWS::EC2::SecurityGroup @@ -189,11 +73,6 @@ Resources: ToPort: 5432 CidrIp: 10.0.0.0/16 Description: Allow PostgreSQL access within VPC only - - IpProtocol: tcp - FromPort: 443 - ToPort: 443 - CidrIp: 0.0.0.0/0 - Description: Allow HTTPS outbound traffic Tags: - Key: Name Value: ServiceLifecycle-Lambda-SecurityGroup @@ -263,8 +142,7 @@ Resources: ServiceLifecycleLambda: Type: AWS::Serverless::Function Properties: - CodeUri: ./LambdaWithServiceLifecycle.zip - # CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/LambdaWithServiceLifecycle/LambdaWithServiceLifecycle.zip + CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/LambdaWithServiceLifecycle/LambdaWithServiceLifecycle.zip Timeout: 60 Handler: swift.bootstrap # ignored by the Swift runtime Runtime: provided.al2 @@ -275,8 +153,8 @@ Resources: SecurityGroupIds: - !Ref LambdaSecurityGroup SubnetIds: - - !Ref PublicSubnet1 - - !Ref PublicSubnet2 + - !Ref PrivateSubnet1 + - !Ref PrivateSubnet2 Environment: Variables: LOG_LEVEL: trace From b06e0df5439599e9816a8ad3958b0c82829d87d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 25 Jul 2025 21:40:43 +0400 Subject: [PATCH 31/38] fix CI --- .github/workflows/pull_request.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 1e541ae6..3364da01 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -36,7 +36,7 @@ jobs: # We pass the list of examples here, but we can't pass an array as argument # Instead, we pass a String with a valid JSON array. # The workaround is mentioned here https://github.com/orgs/community/discussions/11692 - examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'StreamingFromEvent', 'ServiceLifecycle', 'Testing', 'Tutorial' ]" + examples: "[ 'APIGateway', 'APIGateway+LambdaAuthorizer', 'BackgroundTasks', 'HelloJSON', 'HelloWorld', 'ResourcesPackaging', 'S3EventNotifier', 'S3_AWSSDK', 'S3_Soto', 'Streaming', 'StreamingFromEvent', 'ServiceLifecycle+Postgres', 'Testing', 'Tutorial' ]" archive_plugin_examples: "[ 'HelloWorld', 'ResourcesPackaging' ]" archive_plugin_enabled: true From 707f38023617ec31ef3a2916b208da4589af1557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 25 Jul 2025 22:03:14 +0400 Subject: [PATCH 32/38] add a logging statement in case of error --- Sources/AWSLambdaRuntime/Lambda+LocalServer.swift | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index 69db5ab9..c26e33cc 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -224,7 +224,10 @@ internal struct LambdaHTTPServer { } logger.info("Server shutting down") - return result // ignore errors here, we are shutting down anyway + if case .failure(let error) = result { + logger.error("Error during server shutdown: \(error)") + } + return result } /// This method handles individual TCP connections From a7aa99e7c6cd69d7a346df16ea74b663d3981fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Fri, 25 Jul 2025 22:06:08 +0400 Subject: [PATCH 33/38] swift-format --- Sources/AWSLambdaRuntime/Lambda+LocalServer.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index c26e33cc..585de70f 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -226,8 +226,8 @@ internal struct LambdaHTTPServer { logger.info("Server shutting down") if case .failure(let error) = result { logger.error("Error during server shutdown: \(error)") - } - return result + } + return result } /// This method handles individual TCP connections From a31814f6f05d634b722bacefc2b05e9dcca98913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 29 Jul 2025 09:11:38 +0400 Subject: [PATCH 34/38] add sample request for easy testing --- .../events/sample-request.json | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 Examples/ServiceLifecycle+Postgres/events/sample-request.json diff --git a/Examples/ServiceLifecycle+Postgres/events/sample-request.json b/Examples/ServiceLifecycle+Postgres/events/sample-request.json new file mode 100644 index 00000000..e75466fd --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/events/sample-request.json @@ -0,0 +1,49 @@ +{ + "version": "2.0", + "routeKey": "$default", + "rawPath": "/", + "rawQueryString": "", + "body": "", + "headers": { + "x-amzn-tls-cipher-suite": "TLS_AES_128_GCM_SHA256", + "x-amzn-tls-version": "TLSv1.3", + "x-amzn-trace-id": "Root=1-68762f44-4f6a87d1639e7fc356aa6f96", + "x-amz-date": "20250715T103651Z", + "x-forwarded-proto": "https", + "host": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "x-forwarded-port": "443", + "x-forwarded-for": "2a01:...:b9f", + "accept": "*/*", + "user-agent": "curl/8.7.1" + }, + "requestContext": { + "accountId": "0123456789", + "apiId": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "authorizer": { + "iam": { + "accessKey": "AKIA....", + "accountId": "0123456789", + "callerId": "AIDA...", + "cognitoIdentity": null, + "principalOrgId": "o-rlrup7z3ao", + "userArn": "arn:aws:iam::0123456789:user/sst", + "userId": "AIDA..." + } + }, + "domainName": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe.lambda-url.us-east-1.on.aws", + "domainPrefix": "zvnsvhpx7u5gn3l3euimg4jjou0jvbfe", + "http": { + "method": "GET", + "path": "/", + "protocol": "HTTP/1.1", + "sourceIp": "2a01:...:b9f", + "userAgent": "curl/8.7.1" + }, + "requestId": "f942509a-283f-4c4f-94f8-0d4ccc4a00f8", + "routeKey": "$default", + "stage": "$default", + "time": "15/Jul/2025:10:36:52 +0000", + "timeEpoch": 1752575812081 + }, + "isBase64Encoded": false +} \ No newline at end of file From 3fc22866089538c38a7378a8d033c37a706b4f56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 29 Jul 2025 09:12:05 +0400 Subject: [PATCH 35/38] add prelude service to run PG initialization code --- .../Sources/Lambda.swift | 19 ++++--- .../Sources/PreludeService.swift | 57 +++++++++++++++++++ 2 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift diff --git a/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift b/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift index 0b3e6c40..a59654fb 100644 --- a/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift +++ b/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift @@ -47,12 +47,21 @@ struct LambdaFunction { private func main() async throws { // Instantiate LambdaRuntime with a handler implementing the business logic of the Lambda function - let runtime = LambdaRuntime(logger: self.logger, body: self.handler) + 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, runtime], + services: [self.pgClient, preludeService], gracefulShutdownSignals: [.sigterm], cancellationSignals: [.sigint], logger: self.logger @@ -76,12 +85,6 @@ struct LambdaFunction { // 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)) { - // check if table exists - // TODO: ideally, I want to do this once, after serviceGroup.run() is done - // but before the handler is called - logger.trace("Checking database") - try await prepareDatabase() - // query users logger.trace("Querying database") return try await self.queryUsers() diff --git a/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift b/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift new file mode 100644 index 00000000..6ca44af8 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift @@ -0,0 +1,57 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// +//===----------------------------------------------------------------------===// +// +// This source file is part of the Hummingbird server framework project +// +// Copyright (c) 2024 the Hummingbird authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See hummingbird/CONTRIBUTORS.txt for the list of Hummingbird authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +// Copied from https://github.com/hummingbird-project/hummingbird/blob/main/Sources/Hummingbird/Utils/PreludeService.swift + +import ServiceLifecycle + +/// Wrap another service to run after a prelude closure has completed +struct PreludeService: Service, CustomStringConvertible { + let prelude: @Sendable () async throws -> Void + let service: S + + var description: String { + "PreludeService<\(S.self)>" + } + + init(service: S, prelude: @escaping @Sendable () async throws -> Void) { + self.service = service + self.prelude = prelude + } + + func run() async throws { + try await self.prelude() + try await self.service.run() + } +} + +extension Service { + /// Build existential ``PreludeService`` from an existential `Service` + func withPrelude(_ prelude: @escaping @Sendable () async throws -> Void) -> Service { + PreludeService(service: self, prelude: prelude) + } +} \ No newline at end of file From 31edfb0ec52702174e96829be97cb569e3957b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 29 Jul 2025 09:15:59 +0400 Subject: [PATCH 36/38] swift-format --- Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift b/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift index 6ca44af8..a64053a2 100644 --- a/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift +++ b/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift @@ -54,4 +54,4 @@ extension Service { func withPrelude(_ prelude: @escaping @Sendable () async throws -> Void) -> Service { PreludeService(service: self, prelude: prelude) } -} \ No newline at end of file +} From 05a087eb677f0b0a6f7369bc1dcac836ec5c09d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 29 Jul 2025 18:18:23 +0400 Subject: [PATCH 37/38] make explicit that we ignore ChannelError I/O on closed channel during shutdown --- .../AWSLambdaRuntime/Lambda+LocalServer.swift | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index 585de70f..a8735661 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -51,11 +51,21 @@ extension Lambda { logger: Logger, _ body: sending @escaping () async throws -> Void ) async throws { - _ = try await LambdaHTTPServer.withLocalServer( - invocationEndpoint: invocationEndpoint, - logger: logger - ) { - try await body() + do { + try await LambdaHTTPServer.withLocalServer( + invocationEndpoint: invocationEndpoint, + logger: logger + ) { + try await body() + } + } catch let error as ChannelError { + // when this server is part of a ServiceLifeCycle group + // and user presses CTRL-C, this error is thrown + // The error description is "I/O on closed channel" + // TODO: investigate and solve the root cause + // because this server is used only for local tests + // and the error happens when we shutdown the server, I decided to ignore it at the moment. + logger.trace("Ignoring ChannelError during local server shutdown: \(error)") } } } @@ -106,7 +116,7 @@ internal struct LambdaHTTPServer { eventLoopGroup: MultiThreadedEventLoopGroup = .singleton, logger: Logger, _ closure: sending @escaping () async throws -> Result - ) async throws -> Swift.Result { + ) async throws -> Result { let channel = try await ServerBootstrap(group: eventLoopGroup) .serverChannelOption(.backlog, value: 256) .serverChannelOption(.socketOption(.so_reuseaddr), value: 1) @@ -227,7 +237,7 @@ internal struct LambdaHTTPServer { if case .failure(let error) = result { logger.error("Error during server shutdown: \(error)") } - return result + return try result.get() } /// This method handles individual TCP connections From 77ff037d3b443c9bfaef86e1b76d5e38b57e4251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Stormacq?= Date: Tue, 29 Jul 2025 18:24:11 +0400 Subject: [PATCH 38/38] swift-format --- Sources/AWSLambdaRuntime/Lambda+LocalServer.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index a8735661..f536e3f4 100644 --- a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift +++ b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift @@ -59,7 +59,7 @@ extension Lambda { try await body() } } catch let error as ChannelError { - // when this server is part of a ServiceLifeCycle group + // when this server is part of a ServiceLifeCycle group // and user presses CTRL-C, this error is thrown // The error description is "I/O on closed channel" // TODO: investigate and solve the root cause