Learning Rust: Getting started with the AWS SDK for Rust!
TL;DR: CODE
Unlike Python’s Boto3, the AWS SDK for Rust requires explicit configuration setup with aws-config and async runtime (tokio). This guide covers the essential setup steps, credential/region configuration patterns, and how to integrate with LocalStack for local development.
Okay, you’ve decided to dip your toes into Rust! Excellent choice! Oh and you want to build something in the cloud using AWS? How delightful! 👏 Wait what’s that? You need the AWS SDK? Sure, that is not a problem, it’s right here. However, there are a few things you need to know when it comes to using this SDK. Unlike the Boto3 SDK (the SDK for Python) there are a few additional steps you need to take before invoking those API calls. In this post let’s explore how you can configure the AWS Rust SDK and how you can use that SDK with tools such as LocalStack.
Oh, and I’ve also made a little video about this:
To work with the AWS SDK for Rust you need install the appropriate Rust crates. For almost any project you will work with you need to have tokio, aws-config (we will talk about it in a bit), and the actual AWS SDK package you need. Unlike in Boto3 each service has its own SDK crate. So if you want to work with AWS DynamoDB you need the aws-sdk-dynamodb crate.
Here is an example of how I typically set up my new Rust project that will use the AWS SDK for some Amazon S3 shenanigans. This sets up all the needed packages and enables the required features:
cargo add tokio aws-config aws-sdk-s3 \
--features tokio/full,aws-config/behavior-version-latest,aws-config/credentials-loginLet’s modify our main.rs to set up the SDK and the Amazon S3 client:
#[tokio::main] // Macro to set up the main function for Tokio
async fn main() {
// Configure the SDK with defaults:
let sdk_config = aws_config::from_env().load().await;
// Configure the client with the SDK config
let s3_client = aws_sdk_s3::Client::new(&sdk_config);
}A quick note on
tokio: The AWS SDK for Rust, requires an async runtime. Hence the need on thefullfeature flag.
That’s it. Yes! Ready to write to those S3 buckets! 🥳
What credentials and region would be used if we just ran the application like this? I’ll add some tracing and let’s test it out.
cargo add tracing tracing_subscriber --features tracing_subscriber/env-filterHere is what the modified main.rs should look like:
use tracing_subscriber::EnvFilter;
#[tokio::main]
async fn main() {
// Configure tracing:
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into())
)
.with_writer(std::io::stderr)
.with_ansi(true)
.init();
// Configure the SDK:
let sdk_config = aws_config::from_env().load().await;
// Configure the client
let s3_client = aws_sdk_s3::Client::new(&sdk_config);
// Get the current region used:
tracing::info!("Region selected: {:?}", sdk_config.region().unwrap());
// Running any SDK call so the SDK would emit what credentials are used
let _buckets = s3_client.list_buckets().send().await;
}If we run this with cargo run, we should see something like this in the output:
2025-12-22T19:54:31.056334Z INFO sdk_creds: Region selected: Region("us-west-2")
2025-12-22T19:54:31.056967Z INFO aws_config::profile::credentials: constructed abstract provider from config file chain=ProfileChain { base: LoginSession { login_session_arn: "arn:aws:iam::FAKEACCOUNTID:user/darkomesaros" }, chain: [] }
2025-12-22T19:54:31.057306Z INFO aws_config::profile::credentials: loaded base credentials creds=Credentials { provider_name: "Login", access_key_id: "ASIXXXXXXXXXXXXXX", secret_access_key: "** redacted **", expires_after: "2025-12-22T20:06:20Z", account_id: "FAKEACCOUNTID", property_0: TypeErasedBox[Clone]:[CredentialsProfileLogin] }Got it! 👏 We are using us-west-2 as the region and it’s using my login credentials. Both of these settings were obtained by the new feature of aws login.
However, if we had environment variables like AWS_REGION, AWS_ACCESS_KEY_ID, and AWS_SECRET_ACCESS_KEY set. Those **would take preference. Actually here is the order of preference the SDK takes for your credentials and region:
🔐 Credentials:
~/.aws/config, ~/.aws/credentials)🌍 Region:
~/.aws/config, ~/.aws/credentials)Let’s make changes to what it uses. For this, we will be configuring the AWS Region by passing a command line parameter with clap and by selecting a specific profile for our credentials. Please note, this does not make a lot of sense in a real world scenario, as you would likely get these parameters from the same place (ie the profile), but I’ll be extra verbose here so you see the approaches.
To configure the region, we will be using a RegionProviderChain. This allows us to select a region from a series of region providers. Namely we first attempt to load a region from a variable (or a command line parameter in our case), then from the default provider chain, and if all that fails, we fall back to good ol’ “us-east-1”.
Here is the code that does that:
let region = Some(String::from("us-west-2"));
let region_provider = RegionProviderChain::first_try(region.map(Region::new))
.or_default_provider() // Env -> Profile -> EC2 IMDSv2
.or_else(Region::new("us-east-1"));Now we pass this region_provider variable (a RegionProviderChain) to the sdk_config creation process by adding some additional parameters:
let sdk_config = aws_config::from_env()
.region(region_provider) // <- Select the Region
.load().await;We have now selected the region. To expand this, and now select a different credentials profile let’s add one more parameter:
let sdk_config = aws_config::from_env()
.region(region_provider)
.profile_name("personal") // <- Set the profile you wish to use
.load().await;Let’s implement this in our main.rs, here is the fully modified file:
use tracing_subscriber::EnvFilter;
use clap::Parser;
use aws_config::{Region, meta::region::RegionProviderChain};
#[derive(Parser)]
struct Opt {
region: Option<String>
}
#[tokio::main]
async fn main() {
// Configure tracing:
tracing_subscriber::fmt()
.with_env_filter(
EnvFilter::from_default_env()
.add_directive(tracing::Level::INFO.into())
)
.with_writer(std::io::stderr)
.with_ansi(true)
.init();
// Parsing command line parameters
let Opt {
region
} = Opt::parse();
// Configure the region provider chain
let region_provider = RegionProviderChain::first_try(region.map(Region::new))
.or_default_provider() // Env -> Profile -> EC2 IMDSv2
.or_else(Region::new("us-east-1"));
// Configure the SDK:
let sdk_config = aws_config::from_env()
.region(region_provider) // <- Select the region
.profile_name("personal") // <- Set the profile you wish to use
.load().await;
// Configure the client
let s3_client = aws_sdk_s3::Client::new(&sdk_config);
// Get the current region used:
tracing::info!("Region selected: {:?}", sdk_config.region().unwrap());
// Running any SDK call so the SDK would emit what credentials are used
let _buckets = s3_client.list_buckets().send().await;
}To run the application and pass it on the region parameter use the following command:
cargo run "eu-west-1"Barring having entered the wrong region name, or not having a given profile, you should get output similar to this:
2025-12-22T20:27:52.251894Z INFO sdk_creds: Region selected: Region("eu-west-1")
2025-12-22T20:27:52.252756Z INFO aws_config::profile::credentials: constructed abstract provider from config file chain=ProfileChain { base: LoginSession { login_session_arn: "arn:aws:iam::OTHERFAKEACCOUNTID:user/grunf" }, chain: [] }
2025-12-22T20:27:52.253169Z INFO aws_config::profile::credentials: loaded base credentials creds=Credentials { provider_name: "Login", access_key_id: "AYYYYYYYYYYYYYYYY", secret_access_key: "** redacted **", expires_after: "2025-12-22T20:41:47Z", account_id: "OTHERFAKEACCOUNTID", property_0: TypeErasedBox[Clone]:[CredentialsProfileLogin] }Huzzah! 🥳
I’m gonna leave you with one more tip. Making a call to a different API endpoint. By default the AWS SDK makes the API calls to AWS (duh!), but what if you were doing some testing and local development with a tool like LocalStack? Could you make those API calls to there? Absolutely yes!
I won’t get into the details on how to use LocalStack, there are excellent resources for that. But what I will show you is how to modify the SdkConfig object in the AWS SDK for Rust.
It’s really quite simple - just modify our sdk_config variable and include one additional parameter:
let sdk_config = aws_config::from_env()
.region(region_provider)
.profile_name("personal")
.endpoint_url("http://localhost:4566") // For LocalStack
.load().await;This will now target our local LocalStack instance. When using LocalStack, the region does not really matter. Ie the requests do not get routed to the defined region, and will always display results from us-east-1. I’ll just add the following line to display all the buckets I list:
// Running any SDK call so the SDK would emit what credentials are used
let buckets = s3_client.list_buckets().send().await.unwrap();
println!("Buckets: {:#?}", buckets.buckets());Let’s run it:
cargo run us-west-2You should get something like this:
2025-12-22T20:45:14.484685Z INFO sdk_creds: Region selected: Region("us-west-2")
2025-12-22T20:45:14.485379Z INFO aws_config::profile::credentials: constructed abstract provider from config file chain=ProfileChain { base: LoginSession { login_session_arn: "arn:aws:iam::503716878456:user/darkomesaros" }, chain: [] }
2025-12-22T20:45:14.485753Z INFO aws_config::profile::credentials: loaded base credentials creds=Credentials { provider_name: "Login", access_key_id: "ASIAXKR65TB4NUTJXKST", secret_access_key: "** redacted **", expires_after: "2025-12-22T20:52:51Z", account_id: "503716878456", property_0: TypeErasedBox[Clone]:[CredentialsProfileLogin] }
Buckets: [
Bucket {
name: Some(
"local-bucket",
),
creation_date: Some(
2025-12-22T20:43:53Z,
),
bucket_region: Some(
"us-east-1",
),
bucket_arn: None,
},
]Look at that sweet sweet local bucket! 😍
Well, first off you can check out the full code listing. Second, you may notice a lot of .unwrap() methods being invoked here. Now, this is all great for development, but be careful with those when going into production. Stay tuned, for an upcoming blog post on that topic.
Keep learning Rust, it does pay off in the end! 🦀 💘