Skip to content

[WIP] DO NOT MERGE - [example] Add example for Swift Service Lifecycle #522

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Examples/ServiceLifecycle/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.amazonq
56 changes: 56 additions & 0 deletions Examples/ServiceLifecycle/Package.swift
Original file line number Diff line number Diff line change
@@ -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.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"),
],
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)
]
}
210 changes: 210 additions & 0 deletions Examples/ServiceLifecycle/README.md
Original file line number Diff line number Diff line change
@@ -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
103 changes: 103 additions & 0 deletions Examples/ServiceLifecycle/Sources/Lambda.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
//===----------------------------------------------------------------------===//
//
// 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 {
try await LambdaFunction().main()
}

private let pgClient: PostgresClient
private var logger: Logger
private init() throws {
self.logger = Logger(label: "ServiceLifecycleExample")

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() 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)

/// 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 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
// 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)")
}
return "Done"
}

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)
}
}
Loading
Loading