[example] Add example for Swift Service Lifecycle (#522)

Now that task cancellation works, re publishing this PR with a new
example for Swift Service Lifecycle
This commit is contained in:
Sébastien Stormacq
2025-07-30 06:40:55 +04:00
committed by GitHub
parent 36dadf9c26
commit e6ba07fd06
20 changed files with 1409 additions and 9 deletions
+1 -1
View File
@@ -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
+2 -1
View File
@@ -34,4 +34,5 @@ Package.resolved
*.yml
**/.npmignore
**/*.json
**/*.txt
**/*.txt
*.toml
@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc
.amazonq
@@ -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.
@@ -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)
]
}
@@ -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
@@ -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)
}
@@ -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<S: Service>: 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)
}
}
@@ -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
]
@@ -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-----
"""
@@ -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-----
"""
@@ -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<Success: Sendable>(
deadline: Duration,
_ closure: @escaping @Sendable () async throws -> Success
) async throws -> Success {
let clock = ContinuousClock()
let result = await withTaskGroup(of: TimeoutResult<Success>.self, returning: Result<Success, any Error>.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<Success, any Error>?
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<Success: Sendable> {
case deadlineHit
case deadlineCancelled
case workFinished(Result<Success, any Error>)
}
+36
View File
@@ -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)"
@@ -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
}
@@ -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
@@ -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"
@@ -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
+8
View File
@@ -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 = []
@@ -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()
}
+2 -2
View File
@@ -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