aws_utils_secretsmanager 0.4.0

AWS Secrets Manager utilities for retrieving secret values
Documentation

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:

[dependencies]
aws_utils_secretsmanager = "0.1.0"

Usage

Basic Example

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

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

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

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 crate, which is also what the AWS SDK uses internally.

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:

// 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):

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

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

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:

# Optional: Custom Secrets Manager endpoint (e.g., LocalStack)
export SECRETSMANAGER_ENDPOINT_URL=http://localhost:4566

# Run tests
cargo test

Test Commands

# 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

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

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