Skip to content

Commit a565ae4

Browse files
committed
Add S3EventNotifier example
1 parent bd0ec62 commit a565ae4

File tree

4 files changed

+199
-0
lines changed

4 files changed

+199
-0
lines changed

Examples/S3EventNotifier/.gitignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.DS_Store
2+
/.build
3+
/.index-build
4+
/Packages
5+
xcuserdata/
6+
DerivedData/
7+
.swiftpm/configuration/registries.json
8+
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
9+
.netrc
+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// swift-tools-version: 6.0
2+
import PackageDescription
3+
4+
// needed for CI to test the local version of the library
5+
import struct Foundation.URL
6+
7+
let package = Package(
8+
name: "CSVUploadAPINotificationLambda",
9+
platforms: [.macOS(.v15)],
10+
dependencies: [
11+
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main"),
12+
.package(url: "https://github.com/swift-server/swift-aws-lambda-events", branch: "main"),
13+
.package(url: "https://github.com/swift-server/async-http-client.git", from: "1.24.0"),
14+
],
15+
targets: [
16+
.executableTarget(
17+
name: "CSVUploadAPINotificationLambda",
18+
dependencies: [
19+
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime"),
20+
.product(name: "AWSLambdaEvents", package: "swift-aws-lambda-events"),
21+
.product(name: "AsyncHTTPClient", package: "async-http-client"),
22+
]
23+
)
24+
]
25+
)
26+
27+
if let localDepsPath = Context.environment["LAMBDA_USE_LOCAL_DEPS"],
28+
localDepsPath != "",
29+
let v = try? URL(fileURLWithPath: localDepsPath).resourceValues(forKeys: [.isDirectoryKey]),
30+
v.isDirectory == true
31+
{
32+
// when we use the local runtime as deps, let's remove the dependency added above
33+
let indexToRemove = package.dependencies.firstIndex { dependency in
34+
if case .sourceControl(
35+
name: _,
36+
location: "https://github.com/swift-server/swift-aws-lambda-runtime.git",
37+
requirement: _
38+
) = dependency.kind {
39+
return true
40+
}
41+
return false
42+
}
43+
if let indexToRemove {
44+
package.dependencies.remove(at: indexToRemove)
45+
}
46+
47+
// then we add the dependency on LAMBDA_USE_LOCAL_DEPS' path (typically ../..)
48+
print("[INFO] Compiling against swift-aws-lambda-runtime located at \(localDepsPath)")
49+
package.dependencies += [
50+
.package(name: "swift-aws-lambda-runtime", path: localDepsPath)
51+
]
52+
}

Examples/S3EventNotifier/README.md

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
# S3 Event Notifier
2+
3+
This example demonstrates how to create a Lambda that notifies an API of an S3 event in a bucket.
4+
5+
## Code
6+
7+
In this example the lambda function receives an `S3Event` object from the `AWSLambdaEvents` library as input object instead of a `APIGatewayV2Request`. The `S3Event` object contains all the information about the S3 event that triggered the lambda, but what we are interested in is the bucket name and the object key, which are inside of a notification `Record`. The object contains an array of records, however since the lambda is triggered by a single event, we can safely assume that there is only one record in the array: the first one. Inside of this record, we can find the bucket name and the object key:
8+
9+
```swift
10+
guard let s3NotificationRecord = event.records.first else {
11+
throw LambdaError.noNotificationRecord
12+
}
13+
14+
let bucket = s3NotificationRecord.s3.bucket.name
15+
let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ")
16+
```
17+
18+
The key is URL encoded, so we replace the `+` with a space.
19+
20+
Once the event is decoded, the lambda sends a POST request to an API endpoint with the bucket name and the object key as parameters. The API URL is set as an environment variable.
21+
22+
## Build & Package
23+
24+
To build & archive the package you can use the following commands:
25+
26+
```bash
27+
swift build
28+
swift package archive --allow-network-connections docker
29+
```
30+
31+
If there are no errors, a ZIP file should be ready to deploy, located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip`.
32+
33+
## Deploy
34+
35+
To deploy the Lambda function, you can use the `aws` command line:
36+
37+
```bash
38+
aws lambda create-function \
39+
--function-name S3EventNotifier \
40+
--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/S3EventNotifier/S3EventNotifier.zip \
41+
--runtime provided.al2 \
42+
--handler provided \
43+
--architectures arm64 \
44+
--role arn:aws:iam::<YOUR_ACCOUNT_ID>:role/lambda_basic_execution
45+
```
46+
47+
The `--architectures` flag is only required when you build the binary on an Apple Silicon machine (Apple M1 or more recent). It defaults to `x64`.
48+
49+
Be sure to replace <YOUR_ACCOUNT_ID> with your actual AWS account ID (for example: 012345678901).
50+
51+
> [!WARNING]
52+
> You will have to set up an S3 bucket and configure it to send events to the lambda function. This is not covered in this example.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the SwiftAWSLambdaRuntime open source project
4+
//
5+
// Copyright (c) 2024 Apple Inc. and the SwiftAWSLambdaRuntime project authors
6+
// Licensed under Apache License v2.0
7+
//
8+
// See LICENSE.txt for license information
9+
// See CONTRIBUTORS.txt for the list of SwiftAWSLambdaRuntime project authors
10+
//
11+
// SPDX-License-Identifier: Apache-2.0
12+
//
13+
//===----------------------------------------------------------------------===//
14+
15+
import AWSLambdaEvents
16+
import AWSLambdaRuntime
17+
import AsyncHTTPClient
18+
19+
#if canImport(FoundationEssentials)
20+
import FoundationEssentials
21+
#else
22+
import Foundation
23+
#endif
24+
25+
let httpClient = HTTPClient.shared
26+
27+
enum LambdaError: Error {
28+
case noNotificationRecord
29+
case missingEnvVar(name: String)
30+
31+
var description: String {
32+
switch self {
33+
case .noNotificationRecord:
34+
"No notification record in S3 event"
35+
case .missingEnvVar(let name):
36+
"Missing env var named \(name)"
37+
}
38+
}
39+
}
40+
41+
let runtime = LambdaRuntime { (event: S3Event, context: LambdaContext) async throws -> APIGatewayV2Response in
42+
do {
43+
context.logger.debug("Received S3 event: \(event)")
44+
45+
guard let s3NotificationRecord = event.records.first else {
46+
throw LambdaError.noNotificationRecord
47+
}
48+
49+
let bucket = s3NotificationRecord.s3.bucket.name
50+
let key = s3NotificationRecord.s3.object.key.replacingOccurrences(of: "+", with: " ")
51+
52+
guard let apiURL = ProcessInfo.processInfo.environment["API_URL"] else {
53+
throw LambdaError.missingEnvVar(name: "API_URL")
54+
}
55+
56+
let body = """
57+
{
58+
"bucket": "\(bucket)",
59+
"key": "\(key)"
60+
}
61+
"""
62+
63+
context.logger.debug("Sending request to \(apiURL) with body \(body)")
64+
65+
var request = HTTPClientRequest(url: "\(apiURL)/upload-complete/")
66+
request.method = .POST
67+
request.headers = [
68+
"Content-Type": "application/json"
69+
]
70+
request.body = .bytes(.init(string: body))
71+
72+
let response = try await httpClient.execute(request, timeout: .seconds(30))
73+
return APIGatewayV2Response(
74+
statusCode: .ok,
75+
body: "Lambda terminated successfully. API responded with: Status: \(response.status), Body: \(response.body)"
76+
)
77+
} catch let error as LambdaError {
78+
context.logger.error("\(error.description)")
79+
return APIGatewayV2Response(statusCode: .internalServerError, body: "[ERROR] \(error.description)")
80+
} catch {
81+
context.logger.error("\(error)")
82+
return APIGatewayV2Response(statusCode: .internalServerError, body: "[ERROR] \(error)")
83+
}
84+
}
85+
86+
try await runtime.run()

0 commit comments

Comments
 (0)