diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 3194852e..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', '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 diff --git a/.licenseignore b/.licenseignore index d47f45a2..acc480a8 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+Postgres/.gitignore b/Examples/ServiceLifecycle+Postgres/.gitignore new file mode 100644 index 00000000..c35fd53d --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +xcuserdata/ +DerivedData/ +.swiftpm/configuration/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc +.amazonq \ No newline at end of file diff --git a/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md b/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md new file mode 100644 index 00000000..890c7fd2 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/INFRASTRUCTURE.md @@ -0,0 +1,161 @@ +# 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 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 + +### VPC Configuration +- **VPC**: Custom VPC with CIDR block `10.0.0.0/16` +- **DNS Support**: DNS hostnames and DNS resolution enabled + +### Subnet Layout +- **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 and Lambda function + - No public IP addresses assigned + - Complete isolation from internet + +### Network Components +- **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` + +### 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 private subnets with access to database within VPC +- **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 + +This infrastructure implements several security best practices: + +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 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+Postgres/Package.swift b/Examples/ServiceLifecycle+Postgres/Package.swift new file mode 100644 index 00000000..a3d85298 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Package.swift @@ -0,0 +1,58 @@ +// 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.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: [ + .executableTarget( + name: "LambdaWithServiceLifecycle", + dependencies: [ + .product(name: "PostgresNIO", package: "postgres-nio"), + .product(name: "ServiceLifecycle", package: "swift-service-lifecycle"), + .product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"), + .product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"), + ] + ) + ] +) + +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) + ] +} diff --git a/Examples/ServiceLifecycle+Postgres/README.md b/Examples/ServiceLifecycle+Postgres/README.md new file mode 100644 index 00000000..90565bc0 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/README.md @@ -0,0 +1,253 @@ +# A swift Service Lifecycle Lambda function with a managed PostgreSQL database + +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**: 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 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 and cost 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 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. + +5. **Error Handling**: Comprehensive error handling for database connections, queries, and certificate loading. + +## Prerequisites + +- Swift 6.x toolchain +- Docker (for building Lambda functions) +- AWS CLI configured with appropriate permissions +- SAM CLI installed + +## 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 +CREATE TABLE users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) NOT NULL +); + +-- Insert some sample data +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 + +```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 + ``` + +## Getting Connection Details + +After deployment, get the database and API Gateway 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 + +The database is deployed in **private subnets** and is **not directly accessible** from the internet. This follows AWS security best practices. + +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: + +```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_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:$DB_PORT" -U "$DB_USER" -d "$DB_NAME" +``` + +## 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 using SSL/TLS with RDS root certificate verification +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 + +Example response: +```json +[ + {"id": 1, "username": "alice"}, + {"id": 2, "username": "bob"}, + {"id": 3, "username": "charlie"} +] +``` + +## Monitoring + +Check the Lambda function logs: + +```bash +sam logs -n ServiceLifecycleLambda --stack-name servicelifecycle-stack --tail +``` + +## Security Considerations + +โœ… **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. **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. **Minimal Attack Surface**: No public subnets or internet gateways + +The infrastructure implements secure networking patterns suitable for production workloads. + +## Cost Optimization + +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 + +**Estimated cost: ~$16.62/month (or ~$0.45/month with RDS Free Tier)** + +For detailed cost breakdown, see `INFRASTRUCTURE.md`. + +## Cleanup + +To delete all resources: + +```bash +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 + +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 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 + +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 +4. SSL certificates are properly configured for your AWS region + +### Build failures + +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 with ServiceLifecycle integration +- `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 +- `Package.swift`: Swift package definition with PostgresNIO, AWSLambdaRuntime, and ServiceLifecycle dependencies diff --git a/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift b/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift new file mode 100644 index 00000000..a59654fb --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/Sources/Lambda.swift @@ -0,0 +1,192 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the SwiftAWSLambdaRuntime open source project +// +// Copyright (c) 2025 Apple Inc. and the SwiftAWSLambdaRuntime project authors +// Licensed under Apache License v2.0 +// +// See LICENSE.txt for license information +// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +//===----------------------------------------------------------------------===// + +import AWSLambdaEvents +import AWSLambdaRuntime +import Logging +import PostgresNIO +import ServiceLifecycle + +struct User: Codable { + let id: Int + let username: String +} + +@main +struct LambdaFunction { + + private let pgClient: PostgresClient + private let logger: Logger + + private init() throws { + var logger = Logger(label: "ServiceLifecycleExample") + logger.logLevel = Lambda.env("LOG_LEVEL").flatMap(Logger.Level.init) ?? .info + self.logger = logger + + self.pgClient = try LambdaFunction.createPostgresClient( + host: Lambda.env("DB_HOST") ?? "localhost", + user: Lambda.env("DB_USER") ?? "postgres", + password: Lambda.env("DB_PASSWORD") ?? "secret", + dbName: Lambda.env("DB_NAME") ?? "servicelifecycle", + logger: self.logger + ) + } + + /// Function entry point when the runtime environment is created + private func main() async throws { + + // Instantiate LambdaRuntime with a handler implementing the business logic of the Lambda function + let lambdaRuntime = LambdaRuntime(logger: self.logger, body: self.handler) + + // Use a prelude service to execute PG code before setting up the Lambda service + // the PG code will run only once and will create the database schema and populate it with initial data + let preludeService = PreludeService( + service: lambdaRuntime, + prelude: { + try await prepareDatabase() + } + ) + + /// Use ServiceLifecycle to manage the initialization and termination + /// of the PGClient together with the LambdaRuntime + let serviceGroup = ServiceGroup( + services: [self.pgClient, preludeService], + gracefulShutdownSignals: [.sigterm], + cancellationSignals: [.sigint], + logger: self.logger + ) + + // launch the service groups + // this call will return upon termination or cancellation of all the services + try await serviceGroup.run() + + // perform any cleanup here + } + + /// Function handler. This code is called at each function invocation + /// input event is ignored in this demo. + private func handler(event: APIGatewayV2Request, context: LambdaContext) async throws -> APIGatewayV2Response { + + var result: [User] = [] + do { + // IMPORTANT - CURRENTLY, THIS CALL STOPS WHEN DB IS NOT REACHABLE + // See: https://github.com/vapor/postgres-nio/issues/489 + // This is why there is a timeout, as suggested Fabian + // See: https://github.com/vapor/postgres-nio/issues/489#issuecomment-2186509773 + result = try await timeout(deadline: .seconds(3)) { + // query users + logger.trace("Querying database") + return try await self.queryUsers() + } + } catch { + logger.error("Database Error", metadata: ["cause": "\(String(reflecting: error))"]) + } + + return try .init( + statusCode: .ok, + headers: ["content-type": "application/json"], + encodableBody: result + ) + } + + /// Prepare the database + /// At first run, this functions checks the database exist and is populated. + /// This is useful for demo purposes. In real life, the database will contain data already. + private func prepareDatabase() async throws { + do { + + // initial creation of the table. This will fails if it already exists + logger.trace("Testing if table exists") + try await self.pgClient.query(SQLStatements.createTable) + + // it did not fail, it means the table is new and empty + logger.trace("Populate table") + try await self.pgClient.query(SQLStatements.populateTable) + + } catch is PSQLError { + // when there is a database error, it means the table or values already existed + // ignore this error + logger.trace("Table exists already") + } catch { + // propagate other errors + throw error + } + } + + /// Query the database + private func queryUsers() async throws -> [User] { + var users: [User] = [] + let query = SQLStatements.queryAllUsers + let rows = try await self.pgClient.query(query) + for try await (id, username) in rows.decode((Int, String).self) { + self.logger.trace("\(id) : \(username)") + users.append(User(id: id, username: username)) + } + return users + } + + /// Create a postgres client + /// ...TODO + private static func createPostgresClient( + host: String, + user: String, + password: String, + dbName: String, + logger: Logger + ) throws -> PostgresClient { + + // Load the root certificate + let region = Lambda.env("AWS_REGION") ?? "us-east-1" + guard let pem = rootRDSCertificates[region] else { + logger.error("No root certificate found for the specified AWS region.") + throw LambdaErrors.missingRootCertificateForRegion(region) + } + let certificatePEM = Array(pem.utf8) + let rootCert = try NIOSSLCertificate.fromPEMBytes(certificatePEM) + + // Add the root certificate to the TLS configuration + var tlsConfig = TLSConfiguration.makeClientConfiguration() + tlsConfig.trustRoots = .certificates(rootCert) + + // Enable full verification + tlsConfig.certificateVerification = .fullVerification + + let config = PostgresClient.Configuration( + host: host, + port: 5432, + username: user, + password: password, + database: dbName, + tls: .prefer(tlsConfig) + ) + + return PostgresClient(configuration: config) + } + + private struct SQLStatements { + static let createTable: PostgresQuery = + "CREATE TABLE users (id SERIAL PRIMARY KEY, username VARCHAR(50) NOT NULL);" + static let populateTable: PostgresQuery = "INSERT INTO users (username) VALUES ('alice'), ('bob'), ('charlie');" + static let queryAllUsers: PostgresQuery = "SELECT id, username FROM users" + } + + static func main() async throws { + try await LambdaFunction().main() + } + +} + +public enum LambdaErrors: Error { + case missingRootCertificateForRegion(String) +} diff --git a/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift b/Examples/ServiceLifecycle+Postgres/Sources/PreludeService.swift new file mode 100644 index 00000000..a64053a2 --- /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) + } +} diff --git a/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/RootRDSCert.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/RootRDSCert.swift new file mode 100644 index 00000000..f89c5fbb --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/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+Postgres/Sources/RDSCertificates/eu-central-1.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/eu-central-1.swift new file mode 100644 index 00000000..e602ebf2 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/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+Postgres/Sources/RDSCertificates/us-east-1.swift b/Examples/ServiceLifecycle+Postgres/Sources/RDSCertificates/us-east-1.swift new file mode 100644 index 00000000..f68a6781 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/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+Postgres/Sources/Timeout.swift b/Examples/ServiceLifecycle+Postgres/Sources/Timeout.swift new file mode 100644 index 00000000..6a8dc5dc --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/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+Postgres/deploy.sh b/Examples/ServiceLifecycle+Postgres/deploy.sh new file mode 100755 index 00000000..262cbf65 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/deploy.sh @@ -0,0 +1,36 @@ +#!/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 + +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:" +# shellcheck disable=SC2006,SC2016 +echo "curl $(aws cloudformation describe-stacks --stack-name servicelifecycle-stack --query 'Stacks[0].Outputs[?OutputKey==`APIGatewayEndpoint`].OutputValue' --output text)" 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 diff --git a/Examples/ServiceLifecycle+Postgres/localdb.sh b/Examples/ServiceLifecycle+Postgres/localdb.sh new file mode 100644 index 00000000..db615b4d --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/localdb.sh @@ -0,0 +1,45 @@ +#!/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 +## +##===----------------------------------------------------------------------===## + +# 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 servicelifecycle + +# Alternative: Connect using the postgres-db container itself +docker exec -it postgres-db psql -U postgres -d servicelifecycle + diff --git a/Examples/ServiceLifecycle+Postgres/samconfig.toml b/Examples/ServiceLifecycle+Postgres/samconfig.toml new file mode 100644 index 00000000..4171fc12 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/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+Postgres/template.yaml b/Examples/ServiceLifecycle+Postgres/template.yaml new file mode 100644 index 00000000..a9490a33 --- /dev/null +++ b/Examples/ServiceLifecycle+Postgres/template.yaml @@ -0,0 +1,216 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: SAM Template for ServiceLifecycle Lambda with PostgreSQL RDS + +Parameters: + + 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 and Lambda + VPC: + Type: AWS::EC2::VPC + Properties: + CidrBlock: 10.0.0.0/16 + EnableDnsHostnames: true + EnableDnsSupport: true + Tags: + - Key: Name + Value: ServiceLifecycle-VPC + + # 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 + + # Security Group for RDS + DatabaseSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + GroupName: ServiceLifecycle-DB-SG + GroupDescription: Security group for PostgreSQL database + VpcId: !Ref VPC + 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: tcp + FromPort: 5432 + ToPort: 5432 + CidrIp: 10.0.0.0/16 + Description: Allow PostgreSQL access within VPC only + 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 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 + + # PostgreSQL RDS Instance + PostgreSQLDatabase: + Type: AWS::RDS::DBInstance + DeletionPolicy: Delete + Properties: + DBInstanceIdentifier: servicelifecycle-postgres + DBInstanceClass: db.t3.micro + Engine: postgres + EngineVersion: '15.7' + 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: false + BackupRetentionPeriod: 0 + MultiAZ: 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 PrivateSubnet1 + - !Ref PrivateSubnet2 + Environment: + Variables: + 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}}']] + 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" + + DatabaseSecretArn: + Description: ARN of the secret containing database credentials + Value: !Ref DatabaseSecret + Export: + Name: !Sub "${AWS::StackName}-DBSecretArn" + + # 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}-DBConnectionInstructions" + + # Individual connection details for manual connection + ConnectionDetails: + Description: Database connection details + Value: !Sub | + Hostname: ${PostgreSQLDatabase.Endpoint.Address} + Port: ${PostgreSQLDatabase.Endpoint.Port} + Database: ${DBName} + Credentials: Use AWS Secrets Manager to retrieve username and password 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 = [] diff --git a/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift b/Sources/AWSLambdaRuntime/Lambda+LocalServer.swift index b129f6ac..f536e3f4 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)") } } } @@ -224,6 +234,9 @@ internal struct LambdaHTTPServer { } logger.info("Server shutting down") + if case .failure(let error) = result { + logger.error("Error during server shutdown: \(error)") + } return try result.get() } 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