Skip to content

Commit 107fff8

Browse files
committed
basic-sdk example
adds an example for a basic Lambda function that calls S3. this demonstrates the best practice of instantiating an SDK client during function initialization (main method) and reusing that client across multiple invokes.
1 parent 0f6e2a2 commit 107fff8

File tree

3 files changed

+181
-0
lines changed

3 files changed

+181
-0
lines changed

examples/basic-sdk/Cargo.toml

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "basic-sdk"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
7+
8+
[dependencies]
9+
async-trait = "0.1"
10+
aws-config = "0.54"
11+
aws-sdk-s3 = "0.24"
12+
lambda_runtime = { path = "../../lambda-runtime" }
13+
serde = "1.0.136"
14+
tokio = { version = "1", features = ["macros"] }
15+
tracing = { version = "0.1", features = ["log"] }
16+
tracing-subscriber = { version = "0.3", default-features = false, features = ["ansi", "fmt"] }
17+
18+
[dev-dependencies]
19+
mockall = "0.11.3"
20+
tokio-test = "0.4.2"

examples/basic-sdk/README.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
## basic-sdk example
3+
4+
This is an sample function that uses the [AWS SDK](https://github.com/awslabs/aws-sdk-rust) to
5+
list the contents of an S3 bucket specified by the invoker. It uses standard credentials as defined
6+
in the function's execution role to make calls against S3.
7+
8+
### Running Locally
9+
You can use `cargo lambda watch` to spin up a local version of the function. This will automatically re-compile and restart
10+
itself when it observes changes to the code. If you invoke `watch` with no other context then the function will not have
11+
the environment variables necessary to supply on SDK calls. To get around this you can manually pull credentials and provide
12+
them as environment variables on the command line:
13+
```
14+
AWS_ACCESS_KEY_ID="my_akid" AWS_SECRET_ACCESS_KEY="my_secret" AWS_SESSION_TOKEN="my_token" AWS_REGION=us-west-2 cargo lambda watch
15+
```
16+
17+
### Invoking
18+
You can invoke by simply leveraging `cargo lambda invoke` with the payload expected by the function handler.
19+
```
20+
cargo lambda invoke --data-ascii '{"bucket":"my-bucket"}'
21+
```

examples/basic-sdk/src/main.rs

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
use async_trait::async_trait;
2+
use aws_sdk_s3::{output::ListObjectsV2Output, Client as S3Client};
3+
use lambda_runtime::{service_fn, Error, LambdaEvent};
4+
use serde::{Deserialize, Serialize};
5+
6+
/// The request defines what bucket to list
7+
#[derive(Deserialize)]
8+
struct Request {
9+
bucket: String,
10+
}
11+
12+
/// The response contains a Lambda-generated request ID and
13+
/// the list of objects in the bucket.
14+
#[derive(Serialize)]
15+
struct Response {
16+
req_id: String,
17+
bucket: String,
18+
objects: Vec<String>,
19+
}
20+
21+
#[cfg_attr(test, mockall::automock)]
22+
#[async_trait]
23+
trait ListObjects {
24+
async fn list_objects(&self, bucket: &str) -> Result<ListObjectsV2Output, Error>;
25+
}
26+
27+
#[async_trait]
28+
impl ListObjects for S3Client {
29+
async fn list_objects(&self, bucket: &str) -> Result<ListObjectsV2Output, Error> {
30+
self.list_objects_v2().bucket(bucket).send().await.map_err(|e| e.into())
31+
}
32+
}
33+
34+
#[tokio::main]
35+
async fn main() -> Result<(), Error> {
36+
// required to enable CloudWatch error logging by the runtime
37+
tracing_subscriber::fmt()
38+
.with_max_level(tracing::Level::INFO)
39+
// disable printing the name of the module in every log line.
40+
.with_target(false)
41+
// this needs to be set to false, otherwise ANSI color codes will
42+
// show up in a confusing manner in CloudWatch logs.
43+
.with_ansi(false)
44+
// disabling time is handy because CloudWatch will add the ingestion time.
45+
.without_time()
46+
.init();
47+
48+
let shared_config = aws_config::load_from_env().await;
49+
let client = S3Client::new(&shared_config);
50+
let client_ref = &client;
51+
52+
let func = service_fn(move |event| async move { my_handler(event, client_ref).await });
53+
lambda_runtime::run(func).await?;
54+
55+
Ok(())
56+
}
57+
58+
async fn my_handler<T: ListObjects>(event: LambdaEvent<Request>, client: &T) -> Result<Response, Error> {
59+
let bucket = event.payload.bucket;
60+
61+
let objects_rsp = client.list_objects(&bucket).await?;
62+
let objects: Vec<_> = objects_rsp
63+
.contents()
64+
.ok_or("missing objects in list-objects-v2 response")?
65+
.into_iter()
66+
.filter_map(|o| o.key().map(|k| k.to_string()))
67+
.collect();
68+
69+
// prepare the response
70+
let rsp = Response {
71+
req_id: event.context.request_id,
72+
bucket: bucket.clone(),
73+
objects,
74+
};
75+
76+
// return `Response` (it will be serialized to JSON automatically by the runtime)
77+
Ok(rsp)
78+
}
79+
80+
#[cfg(test)]
81+
mod tests {
82+
use super::*;
83+
use aws_sdk_s3::model::Object;
84+
use lambda_runtime::{Context, LambdaEvent};
85+
use mockall::predicate::eq;
86+
87+
#[tokio::test]
88+
async fn response_is_good_for_good_bucket() {
89+
let mut context = Context::default();
90+
context.request_id = "test-request-id".to_string();
91+
92+
let mut mock_client = MockListObjects::default();
93+
mock_client
94+
.expect_list_objects()
95+
.with(eq("test-bucket"))
96+
.returning(|_| {
97+
Ok(ListObjectsV2Output::builder()
98+
.contents(Object::builder().key("test-key-0").build())
99+
.contents(Object::builder().key("test-key-1").build())
100+
.contents(Object::builder().key("test-key-2").build())
101+
.build())
102+
});
103+
104+
let payload = Request {
105+
bucket: "test-bucket".to_string(),
106+
};
107+
let event = LambdaEvent { payload, context };
108+
109+
let result = my_handler(event, &mock_client).await.unwrap();
110+
111+
let expected_keys = vec![
112+
"test-key-0".to_string(),
113+
"test-key-1".to_string(),
114+
"test-key-2".to_string(),
115+
];
116+
assert_eq!(result.req_id, "test-request-id".to_string());
117+
assert_eq!(result.bucket, "test-bucket".to_string());
118+
assert_eq!(result.objects, expected_keys);
119+
}
120+
121+
#[tokio::test]
122+
async fn response_is_bad_for_bad_bucket() {
123+
let mut context = Context::default();
124+
context.request_id = "test-request-id".to_string();
125+
126+
let mut mock_client = MockListObjects::default();
127+
mock_client
128+
.expect_list_objects()
129+
.with(eq("unknown-bucket"))
130+
.returning(|_| Err(Error::from("test-sdk-error")));
131+
132+
let payload = Request {
133+
bucket: "unknown-bucket".to_string(),
134+
};
135+
let event = LambdaEvent { payload, context };
136+
137+
let result = my_handler(event, &mock_client).await;
138+
assert!(result.is_err());
139+
}
140+
}

0 commit comments

Comments
 (0)