-
Notifications
You must be signed in to change notification settings - Fork 113
Add example and README for a streaming Lambda function #415
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
Merged
Merged
Changes from 5 commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
8959fdc
add streaming example and doc
sebsto 6e0975d
add streaming example to CI
sebsto 0bd77ea
fix soundness
sebsto 3275952
remove unused files
sebsto ea3bcf6
correct usage of zsh env var
sebsto 2bff0a9
typo
sebsto b7865a8
typo
sebsto 7128b1c
add comments
sebsto d3890ae
typo
sebsto c95f635
add an example README with common sections + link to each example
sebsto 010f6b5
Merge branch 'main' into sebsto/streaming
sebsto File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/configuration/registries.json | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata | ||
.netrc |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
// swift-tools-version:6.0 | ||
|
||
import PackageDescription | ||
|
||
// needed for CI to test the local version of the library | ||
import struct Foundation.URL | ||
|
||
#if os(macOS) | ||
let platforms: [PackageDescription.SupportedPlatform]? = [.macOS(.v15)] | ||
#else | ||
let platforms: [PackageDescription.SupportedPlatform]? = nil | ||
#endif | ||
|
||
let package = Package( | ||
name: "swift-aws-lambda-runtime-example", | ||
platforms: platforms, | ||
products: [ | ||
.executable(name: "StreamingNumbers", targets: ["StreamingNumbers"]) | ||
], | ||
dependencies: [ | ||
// during CI, the dependency on local version of swift-aws-lambda-runtime is added dynamically below | ||
.package(url: "https://github.com/swift-server/swift-aws-lambda-runtime.git", branch: "main") | ||
], | ||
targets: [ | ||
.executableTarget( | ||
name: "StreamingNumbers", | ||
dependencies: [ | ||
.product(name: "AWSLambdaRuntime", package: "swift-aws-lambda-runtime") | ||
], | ||
path: "." | ||
) | ||
] | ||
) | ||
|
||
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) | ||
] | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,217 @@ | ||
# Streaming Lambda function | ||
|
||
You can configure your Lambda function to stream response payloads back to clients. Response streaming can benefit latency sensitive applications by improving time to first byte (TTFB) performance. This is because you can send partial responses back to the client as they become available. Additionally, you can use response streaming to build functions that return larger payloads. Response stream payloads have a soft limit of 20 MB as compared to the 6 MB limit for buffered responses. Streaming a response also means that your function doesn’t need to fit the entire response in memory. For very large responses, this can reduce the amount of memory you need to configure for your function. | ||
|
||
Streaming responses incurs a cost. For more information, see [AWS Lambda Pricing](https://aws.amazon.com/lambda/pricing/). | ||
|
||
You can stream responses through [Lambda function URLs](https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html), the AWS SDK, or using the Lambda [InvokeWithResponseStream](https://docs.aws.amazon.com/lambda/latest/dg/API_InvokeWithResponseStream.html) API. In this example, we create an authenticated Lambda function URL. | ||
|
||
|
||
## Code | ||
|
||
The sample code creates a `SendNumbersWithPause` struct that conforms to the `StreamingLambdaHandler` protocol provided by the Swift AWS Lambda Runtime. | ||
|
||
The `handle(...)` method of this protocol receives incoming events as a Swift NIO `ByteBuffer` and returns the output as a `ByteBuffer`. | ||
|
||
The response is streamed through the `LambdaResponseStreamWriter`, which is passed as an argument in the `handle` function. The code calls the `write(_:)` function of the `LambdaResponseStreamWriter` with partial data repeatedly written before | ||
finally closing the response stream by calling `finish()`. Developers can also choose to return the entire output and not | ||
stream the response by calling `writeAndFinish(_:)`. | ||
|
||
An error is thrown if `finish()` is called multiple times or if it is called after having called `writeAndFinish(_:)`. | ||
|
||
The `handle(...)` method is marked as `mutating` to allow handlers to be implemented with a `struct`. | ||
|
||
Once the struct is created and the `handle(...)` method is defined, the sample code creates a `LambdaRuntime` struct and initializes it with the handler just created. Then, the code calls `run()` to start the interaction with the AWS Lambda control plane. | ||
|
||
## Build & Package | ||
|
||
To build & archive the package, type the following commands. | ||
|
||
```bash | ||
swift package archive --allow-network-connections docker | ||
``` | ||
|
||
If there is no error, there is a ZIP file ready to deploy. | ||
The ZIP file is located at `.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip` | ||
|
||
## Deploy with the AWS CLI | ||
|
||
Here is how to deploy using the `aws` command line. | ||
|
||
### Step 1: Create the function | ||
```bash | ||
AWS_ACCOUNT_ID=012345678901 | ||
aws lambda create-function \ | ||
--function-name StreamingNumbers \ | ||
--zip-file fileb://.build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip \ | ||
--runtime provided.al2 \ | ||
--handler provided \ | ||
--architectures arm64 \ | ||
--role arn:aws:iam::${AWS_ACCOUNT_ID}:role/lambda_basic_execution \ | ||
--timeout 15 | ||
``` | ||
|
||
> [!IMPORTANT] | ||
> The timeout value must be bigger than the time it takes for your function to stream its output. Otherwise, the Lambda control plane will terminate the execution environment before your code has a chance to finish writing the stream. Here, teh sample function stream responses during 10 seconds and we set the timeout for 15 seconds. | ||
sebsto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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`. | ||
|
||
Be sure to set `AWS_ACCOUNT_ID` with your actual AWS account ID (for example: 012345678901). | ||
|
||
### Step2: Give permission to invoke that function through an URL | ||
|
||
Anyone with a valid signature from your AWS account will have permission to invoke the function through its URL. | ||
|
||
```bash | ||
aws lambda add-permission \ | ||
--function-name StreamingNumbers \ | ||
--action lambda:InvokeFunctionUrl \ | ||
--principal $AWS_ACCOUNT_ID \ | ||
--function-url-auth-type AWS_IAM \ | ||
--statement-id allowURL | ||
``` | ||
|
||
Be sure to replace <YOUR_ACCOUNT_ID> with your actual AWS account ID (for example: 012345678901). | ||
sebsto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Step3: Create the URL | ||
|
||
This creates [an URL with IAM authentication](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html). Only calls with a valid signature will be authorized. | ||
sebsto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```bash | ||
aws lambda create-function-url-config \ | ||
--function-name StreamingNumbers \ | ||
--auth-type AWS_IAM \ | ||
--invoke-mode RESPONSE_STREAM | ||
``` | ||
This calls return various information, including the URL to invoke your function. | ||
|
||
```json | ||
{ | ||
"FunctionUrl": "https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/", | ||
"FunctionArn": "arn:aws:lambda:us-east-1:012345678901:function:StreamingNumbers", | ||
"AuthType": "AWS_IAM", | ||
"CreationTime": "2024-10-22T07:57:23.112599Z", | ||
"InvokeMode": "RESPONSE_STREAM" | ||
} | ||
``` | ||
|
||
### Invoke your Lambda function | ||
|
||
To invoke the Lambda function, use `curl` with the AWS Sigv4 option to generate the signature. | ||
```bash | ||
URL=https://ul3nf4dogmgyr7ffl5r5rs22640fwocc.lambda-url.us-east-1.on.aws/ | ||
REGION=us-east-1 | ||
ACCESS_KEY=AK... | ||
SECRET_KEY=... | ||
AWS_SESSION_TOKEN=... | ||
sebsto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
curl "$URL" \ | ||
--user "$ACCESS_KEY":"$SECRET_KEY" \ | ||
--aws-sigv4 "aws:amz:$REGION:lambda" \ | ||
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \ | ||
--no-buffer | ||
``` | ||
|
||
Note that there is no payload required for this example. | ||
|
||
This should output the following result, with a one-second delay between each numbers. | ||
|
||
``` | ||
1 | ||
2 | ||
3 | ||
4 | ||
5 | ||
6 | ||
7 | ||
8 | ||
9 | ||
10 | ||
``` | ||
|
||
### Undeploy | ||
|
||
When done testing, you can delete the Lambda function with this command. | ||
|
||
```bash | ||
aws lambda delete-function --function-name StreamingNumbers | ||
``` | ||
|
||
## Deploy with AWS SAM | ||
|
||
Alternatively, you can use [AWS SAM](https://aws.amazon.com/serverless/sam/) to deploy the Lambda function. | ||
|
||
**Prerequisites** : Install the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) | ||
|
||
### SAM Template | ||
|
||
The template file is provided as part of the example in the `template.yaml` file. It defines a Lambda function based on the binary ZIP file. It creates the function url with IAM authentication and sets the function timeout to 15 seconds. | ||
|
||
```yaml | ||
AWSTemplateFormatVersion: '2010-09-09' | ||
Transform: AWS::Serverless-2016-10-31 | ||
Description: SAM Template for StreamingLambda Example | ||
|
||
Resources: | ||
# Lambda function | ||
StreamingNumbers: | ||
Type: AWS::Serverless::Function | ||
Properties: | ||
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip | ||
Timeout: 15 | ||
Handler: swift.bootstrap # ignored by the Swift runtime | ||
Runtime: provided.al2 | ||
MemorySize: 128 | ||
Architectures: | ||
- arm64 | ||
FunctionUrlConfig: | ||
AuthType: AWS_IAM | ||
InvokeMode: RESPONSE_STREAM | ||
|
||
Outputs: | ||
# print Lambda function URL | ||
LambdaURL: | ||
Description: Lambda URL | ||
Value: !GetAtt StreamingNumbersUrl.FunctionUrl | ||
``` | ||
|
||
### Deploy with SAM | ||
|
||
```bash | ||
sam deploy \ | ||
--resolve-s3 \ | ||
--template-file template.yaml \ | ||
--stack-name StreamingNumbers \ | ||
--capabilities CAPABILITY_IAM | ||
``` | ||
|
||
The URL of the function is provided as part of the output. | ||
|
||
``` | ||
CloudFormation outputs from deployed stack | ||
----------------------------------------------------------------------------------------------------------------------------- | ||
Outputs | ||
----------------------------------------------------------------------------------------------------------------------------- | ||
Key LambdaURL | ||
Description Lambda URL | ||
Value https://gaudpin2zjqizfujfnqxstnv6u0czrfu.lambda-url.us-east-1.on.aws/ | ||
----------------------------------------------------------------------------------------------------------------------------- | ||
``` | ||
|
||
Once the function is deployed, you can invoke it with `curl`, similarly to what you did when deploying with the AWS CLI. | ||
|
||
```bash | ||
curl "$URL" \ | ||
--user "$ACCESS_KEY":"$SECRET_KEY" \ | ||
--aws-sigv4 "aws:amz:$REGION:lambda" \ | ||
-H "x-amz-security-token: $AWS_SESSION_TOKEN" \ | ||
--no-buffer | ||
``` | ||
|
||
### Undeploy with SAM | ||
|
||
When done testing, you can delete the infrastructure with this command. | ||
|
||
```bash | ||
sam delete | ||
``` |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// This source file is part of the SwiftAWSLambdaRuntime open source project | ||
// | ||
// Copyright (c) 2024 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 NIOCore | ||
|
||
struct SendNumbersWithPause: StreamingLambdaHandler { | ||
func handle( | ||
_ event: ByteBuffer, | ||
responseWriter: some LambdaResponseStreamWriter, | ||
context: LambdaContext | ||
) async throws { | ||
for i in 1...10 { | ||
// Send partial data | ||
try await responseWriter.write(ByteBuffer(string: "\(i)\n")) | ||
// Perform some long asynchronous work | ||
try await Task.sleep(for: .milliseconds(1000)) | ||
} | ||
// All data has been sent. Close off the response stream. | ||
try await responseWriter.finish() | ||
} | ||
} | ||
|
||
let runtime = LambdaRuntime.init(handler: SendNumbersWithPause()) | ||
try await runtime.run() |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
AWSTemplateFormatVersion: '2010-09-09' | ||
Transform: AWS::Serverless-2016-10-31 | ||
Description: SAM Template for QuoteService | ||
sebsto marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
Resources: | ||
# Lambda function | ||
StreamingNumbers: | ||
Type: AWS::Serverless::Function | ||
Properties: | ||
CodeUri: .build/plugins/AWSLambdaPackager/outputs/AWSLambdaPackager/StreamingNumbers/StreamingNumbers.zip | ||
Timeout: 15 | ||
Handler: swift.bootstrap # ignored by the Swift runtime | ||
Runtime: provided.al2 | ||
MemorySize: 128 | ||
Architectures: | ||
- arm64 | ||
FunctionUrlConfig: | ||
AuthType: AWS_IAM | ||
InvokeMode: RESPONSE_STREAM | ||
|
||
Outputs: | ||
# print Lambda function URL | ||
LambdaURL: | ||
Description: Lambda URL | ||
Value: !GetAtt StreamingNumbersUrl.FunctionUrl |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.