# aws_utils_secretsmanager
AWS Secrets Manager utilities for retrieving secret values from AWS Secrets Manager.
## Features
- Simple interface for retrieving secrets from AWS Secrets Manager
- Support for secret versioning with version ID and version stage
- Custom error handling with detailed error types
- Support for custom AWS endpoints (useful for testing with LocalStack)
- Support for AWS SDK's default credential chain
## Installation
Add this to your `Cargo.toml`:
```toml
[dependencies]
aws_utils_secretsmanager = "0.1.0"
```
## Usage
### Basic Example
```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create Secrets Manager client with default timeout configuration
let client = make_client_with_timeout_default(None).await;
// Get secret value
let secret = get_secret_value(&client, "my-secret-name").await?;
println!("Secret value: {}", secret);
Ok(())
}
```
### Using Custom Endpoint
```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with custom endpoint (e.g., LocalStack)
let client = make_client_with_timeout_default(Some("http://localhost:4566".to_string())).await;
let secret = get_secret_value(&client, "test-secret").await?;
println!("Secret value: {}", secret);
Ok(())
}
```
### Using Custom Timeout Configuration
```rust
use std::time::Duration;
use aws_utils_secretsmanager::{make_client_with_timeout, secretsmanager::get_secret_value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Create client with custom timeout settings
let client = make_client_with_timeout(
None,
Some(Duration::from_secs(5)), // 5 second connect timeout
Some(Duration::from_secs(30)), // 30 second operation timeout
Some(Duration::from_secs(25)), // 25 second operation attempt timeout
Some(Duration::from_secs(20)), // 20 second read timeout
).await;
let secret = get_secret_value(&client, "test-secret").await?;
println!("Secret value: {}", secret);
Ok(())
}
```
### Using with TimeoutConfig
```rust
use aws_config::timeout::{TimeoutConfig, TimeoutConfigBuilder};
use aws_utils_secretsmanager::{make_client, secretsmanager::get_secret_value};
use std::time::Duration;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Build custom timeout configuration
let timeout_config = TimeoutConfigBuilder::new()
.connect_timeout(Duration::from_secs(10))
.operation_timeout(Duration::from_secs(120))
.build();
// Create client with custom timeout configuration
let client = make_client(None, Some(timeout_config), None).await;
let secret = get_secret_value(&client, "long-running-secret").await?;
println!("Secret value: {}", secret);
Ok(())
}
```
### Logging AWS Communication
`make_client` accepts an optional [`SharedInterceptor`]. By passing an interceptor that
implements `aws_sdk_secretsmanager::config::Intercept`, you can run custom logic — such as
logging — every time the client communicates with AWS.
The interceptor below logs each request, response, and operation result. It uses the
[`tracing`](https://crates.io/crates/tracing) crate, which is also what the AWS SDK uses
internally.
```rust
use aws_utils_secretsmanager::make_client;
use aws_sdk_secretsmanager::config::{
ConfigBag, Intercept, RuntimeComponents, SharedInterceptor,
interceptors::{
AfterDeserializationInterceptorContextRef, BeforeDeserializationInterceptorContextRef,
BeforeTransmitInterceptorContextRef,
},
};
type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
#[derive(Debug, Clone)]
struct LoggingInterceptor;
impl Intercept for LoggingInterceptor {
fn name(&self) -> &'static str {
"SecretsManagerLoggingInterceptor"
}
// Called just before each HTTP request is sent (once per retry attempt).
fn read_before_transmit(
&self,
context: &BeforeTransmitInterceptorContextRef<'_>,
_runtime_components: &RuntimeComponents,
_cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let request = context.request();
tracing::info!(
method = %request.method(),
uri = %request.uri(),
"SecretsManager -> AWS request"
);
Ok(())
}
// Called right after each HTTP response is received.
fn read_before_deserialization(
&self,
context: &BeforeDeserializationInterceptorContextRef<'_>,
_runtime_components: &RuntimeComponents,
_cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
let response = context.response();
tracing::info!(status = %response.status(), "AWS -> SecretsManager response");
Ok(())
}
// Called once when the operation completes (after retries), with success or error.
fn read_after_deserialization(
&self,
context: &AfterDeserializationInterceptorContextRef<'_>,
_runtime_components: &RuntimeComponents,
_cfg: &mut ConfigBag,
) -> Result<(), BoxError> {
match context.output_or_error() {
Ok(_) => tracing::info!("SecretsManager operation succeeded"),
Err(err) => tracing::warn!(error = %err, "SecretsManager operation failed"),
}
Ok(())
}
}
# async fn run() {
// Pass the interceptor as the third argument.
let client = make_client(None, None, Some(SharedInterceptor::new(LoggingInterceptor))).await;
# }
```
`tracing` does not emit anything until a subscriber is initialized. Set one up once in your
application (for example with `tracing-subscriber`) and control verbosity with `RUST_LOG`:
```rust
// Add `tracing-subscriber` to your dependencies.
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "info".into()),
)
.init();
```
Example output (`RUST_LOG=info`):
```text
INFO SecretsManagerLoggingInterceptor: SecretsManager -> AWS request method=POST uri=https://secretsmanager.ap-northeast-1.amazonaws.com/
INFO SecretsManagerLoggingInterceptor: AWS -> SecretsManager response status=200
INFO SecretsManagerLoggingInterceptor: SecretsManager operation succeeded
```
### Getting Raw Secret Output with Versioning
```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value_raw};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = make_client_with_timeout_default(None).await;
// Get specific version of secret
let output = get_secret_value_raw(
&client,
Some("my-secret-name"),
Some("version-uuid"), // Version ID
Some("AWSCURRENT") // Version stage
).await?;
if let Some(secret_string) = output.secret_string() {
println!("Secret: {}", secret_string);
}
if let Some(version_id) = output.version_id() {
println!("Version ID: {}", version_id);
}
Ok(())
}
```
### Getting Latest Secret Version
```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value_raw};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = make_client_with_timeout_default(None).await;
// Get current version (default behavior)
let output = get_secret_value_raw(
&client,
Some("my-secret-name"),
None::<String>, // No specific version ID
Some("AWSCURRENT") // Get current version
).await?;
println!("Current secret: {:?}", output.secret_string());
Ok(())
}
```
## API Reference
### Functions
#### `make_client_with_timeout_default(endpoint_url: Option<String>) -> Client`
Creates an AWS Secrets Manager client with default timeout configuration.
- `endpoint_url`: Optional custom endpoint URL for testing (e.g., LocalStack)
- Returns: Configured AWS Secrets Manager Client with default timeouts
- Default timeouts:
- Connect timeout: 3100 seconds
- Operation timeout: 60 seconds
- Operation attempt timeout: 55 seconds
- Read timeout: 50 seconds
#### `make_client_with_timeout(endpoint_url: Option<String>, connect_timeout: Option<Duration>, operation_timeout: Option<Duration>, operation_attempt_timeout: Option<Duration>, read_timeout: Option<Duration>) -> Client`
Creates an AWS Secrets Manager client with custom timeout configuration.
- `endpoint_url`: Optional custom endpoint URL for testing (e.g., LocalStack)
- `connect_timeout`: Optional timeout for establishing connections
- `operation_timeout`: Optional timeout for entire operations
- `operation_attempt_timeout`: Optional timeout for individual operation attempts
- `read_timeout`: Optional timeout for reading responses
- Returns: Configured AWS Secrets Manager Client with custom timeouts
#### `make_client(endpoint_url: Option<String>, timeout_config: Option<TimeoutConfig>, interceptor: Option<SharedInterceptor>) -> Client`
Creates an AWS Secrets Manager client with optional custom endpoint URL, timeout configuration, and interceptor.
- `endpoint_url`: Optional custom endpoint URL for testing (e.g., LocalStack)
- `timeout_config`: Optional timeout configuration
- `interceptor`: Optional interceptor for running custom logic (e.g. logging) on every AWS communication
- Returns: Configured AWS Secrets Manager Client
#### `get_secret_value(client: &Client, secret_id: &str) -> Result<String, Error>`
Retrieves a secret value as a string from the current version.
- `client`: AWS Secrets Manager client
- `secret_id`: Secret identifier (name or ARN)
- Returns: Secret value as String
#### `get_secret_value_raw(client: &Client, secret_id: Option<impl Into<String>>, version_id: Option<impl Into<String>>, version_stage: Option<impl Into<String>>) -> Result<GetSecretValueOutput, Error>`
Retrieves raw secret output from AWS Secrets Manager with version control.
- `client`: AWS Secrets Manager client
- `secret_id`: Optional secret identifier (name or ARN)
- `version_id`: Optional version UUID to retrieve specific version
- `version_stage`: Optional version stage (e.g., "AWSCURRENT", "AWSPENDING")
- Returns: Raw GetSecretValueOutput from AWS SDK
### Error Types
The crate defines custom error types:
- `Error::BuildError`: AWS SDK build errors
- `Error::AwsSdk`: AWS SDK service errors
- `Error::ValidationError`: Validation errors
- `Error::NotFound`: Secret not found
## Secret Versioning
AWS Secrets Manager supports versioning of secrets. You can:
- Get the current version using `"AWSCURRENT"` stage
- Get the pending version using `"AWSPENDING"` stage
- Get a specific version using the version UUID
- Let AWS choose the version by omitting version parameters
### Version Stages
- `AWSCURRENT`: The current version of the secret
- `AWSPENDING`: The version that will become current after rotation completes
- Custom stages: You can define custom version stages for your workflow
## Testing
Set up your test environment:
```bash
# Optional: Custom Secrets Manager endpoint (e.g., LocalStack)
export SECRETSMANAGER_ENDPOINT_URL=http://localhost:4566
# Run tests
cargo test
```
### Test Commands
```bash
# Run all tests
cargo test
# Run with logging
RUST_LOG=info cargo test -- --nocapture
# Run specific test
cargo test test_get_secret_value -- --nocapture
```
## Authentication
The client uses the AWS SDK's default credential chain for authentication:
- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_REGION`)
- ECS task role (for Fargate/ECS)
- EC2 instance profile
- AWS credentials file
- Other configured credential providers
## Use Cases
### Database Credentials
```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};
use serde_json::Value;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = make_client_with_timeout_default(None).await;
let secret_json = get_secret_value(&client, "prod/db/credentials").await?;
let credentials: Value = serde_json::from_str(&secret_json)?;
let username = credentials["username"].as_str().unwrap();
let password = credentials["password"].as_str().unwrap();
// Use credentials to connect to database
println!("Connecting as user: {}", username);
Ok(())
}
```
### API Keys
```rust
use aws_utils_secretsmanager::{make_client_with_timeout_default, secretsmanager::get_secret_value};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = make_client_with_timeout_default(None).await;
let api_key = get_secret_value(&client, "prod/external-api/key").await?;
// Use API key for external service calls
println!("API Key retrieved successfully");
Ok(())
}
```
## License
MIT