serviceconf 0.1.2

Configuration library for microservices: load settings from environment variables with file-based secrets support
Documentation

serviceconf

Build Crates.io Version docs.rs

Environment variable configuration with file-based secrets support

Load configuration from environment variables with native support for file-based secrets (Kubernetes Secrets, Docker Secrets). This is the primary feature that distinguishes serviceconf from other environment variable configuration libraries.

Key Feature: File-based Secrets

The #[conf(from_file)] attribute allows reading secrets from files mounted by Kubernetes or Docker, avoiding the security risks of exposing secrets directly in environment variables:

use serviceconf::ServiceConf;

#[derive(ServiceConf)]
struct Config {
    #[conf(from_file)]
    pub api_key: String,  // Reads from API_KEY or API_KEY_FILE

    #[conf(from_file)]
    pub database_password: String,
}

Why file-based secrets?

  • More secure: Secrets stored in files, not environment variables (which can leak in logs, process lists, etc.)
  • Kubernetes native: Works seamlessly with Kubernetes Secrets mounting
  • Docker Secrets: Direct support for Docker Swarm secrets
  • Flexible: Falls back to direct environment variables for local development

Loading priority:

  1. Direct env var (API_KEY) - for local development
  2. File path from env var (API_KEY_FILE) - for production

Kubernetes Secret example:

apiVersion: v1
kind: Secret
metadata:
  name: app-secrets
type: Opaque
stringData:
  api-key: "prod-api-key-123"
  db-password: "secure-password"
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myservice
spec:
  template:
    spec:
      containers:
      - name: app
        image: myservice:latest
        env:
          - name: API_KEY_FILE
            value: /etc/secrets/api-key
          - name: DATABASE_PASSWORD_FILE
            value: /etc/secrets/db-password
        volumeMounts:
          - name: secrets
            mountPath: /etc/secrets
            readOnly: true
      volumes:
        - name: secrets
          secret:
            secretName: app-secrets
            items:
              - key: api-key
                path: api-key
              - key: db-password
                path: db-password

Local development (no files needed):

export API_KEY=dev-key-123
export DATABASE_PASSWORD=dev-password

Installation

[dependencies]
serviceconf = { git = "https://github.com/lambdalisue/rs-serviceconf" }

Quick Start

use serviceconf::ServiceConf;

#[derive(Debug, ServiceConf)]
struct Config {
    #[conf(from_file)]
    pub api_key: String,

    #[conf(default = 8080)]
    pub port: u16,
}

fn main() -> anyhow::Result<()> {
    let config = Config::from_env()?;
    println!("Port: {}", config.port);
    Ok(())
}

Local development (direct environment variable):

export API_KEY=dev-key-123
export PORT=3000

Production (Kubernetes/Docker with file-based secret):

export API_KEY_FILE=/run/secrets/api-key
export PORT=8080

Other Features

Default Values

Use #[conf(default)] for Default::default() or #[conf(default = value)] for explicit values.

#[derive(ServiceConf)]
struct Config {
    #[conf(default = 8080)]
    pub port: u16,  // 8080 if PORT not set

    #[conf(default = "localhost".to_string())]
    pub host: String,  // "localhost" if HOST not set
}

Optional Fields

Use Option<T> for optional fields. Returns None if not set.

#[derive(ServiceConf)]
struct Config {
    pub api_key: Option<String>,  // None if API_KEY not set
}

Prefix

Use #[conf(prefix = "...")] at struct level to prefix all environment variables.

#[derive(ServiceConf)]
#[conf(prefix = "MYAPP_")]
struct Config {
    pub database_url: String,  // Reads from MYAPP_DATABASE_URL
    pub api_key: String,       // Reads from MYAPP_API_KEY
}
export MYAPP_DATABASE_URL=postgres://localhost/db
export MYAPP_API_KEY=secret123

Custom Environment Variable Names

Use #[conf(name = "...")] to override the auto-generated name.

#[derive(ServiceConf)]
struct Config {
    #[conf(name = "POSTGRES_URL")]
    pub database_url: String,  // Reads from POSTGRES_URL, not DATABASE_URL
}

Custom Deserializers

Use #[conf(deserializer = "function")] for complex types or custom parsing.

// Custom parser
fn comma_separated(s: &str) -> Result<Vec<String>, String> {
    Ok(s.split(',').map(|s| s.trim().to_string()).collect())
}

#[derive(ServiceConf)]
struct Config {
    // JSON array
    #[conf(deserializer = "serde_json::from_str")]
    pub tags: Vec<String>,

    // Comma-separated
    #[conf(deserializer = "comma_separated")]
    pub features: Vec<String>,

    // TOML (requires toml crate)
    #[conf(deserializer = "toml::from_str")]
    pub settings: MySettings,
}
export TAGS='["prod","api","v2"]'
export FEATURES=feature1,feature2,feature3

Attribute Reference

Struct-level Attributes

Attribute Description
#[conf(prefix = "PREFIX_")] Add prefix to all environment variable names

Field-level Attributes

Attribute Description When to Use
#[conf(name = "VAR")] Override environment variable name When field name differs from desired env var
#[conf(default)] Use Default::default() if not set For optional fields with sensible defaults
#[conf(default = value)] Use explicit default value When you need a specific default
#[conf(from_file)] Support {VAR}_FILE pattern For secrets stored in files
#[conf(deserializer = "fn")] Use custom parser For complex types (Vec, HashMap, etc.)

Type Behavior

Type When Env Var Missing When Env Var Set
T (no attribute) Error Parsed with FromStr
T + #[conf(default)] Default::default() Parsed with FromStr
T + #[conf(default = value)] Uses value Parsed with FromStr
Option<T> None Some(parsed_value)
T + #[conf(deserializer = "fn")] Error Parsed with custom function

Combining Attributes

Multiple attributes can be combined:

#[derive(ServiceConf)]
#[conf(prefix = "APP_")]
struct Config {
    // Combines: prefix + custom name + from_file + Option
    #[conf(name = "DB_URL")]
    #[conf(from_file)]
    pub database_url: Option<String>,  // Reads from APP_DB_URL or APP_DB_URL_FILE

    // Combines: prefix + default
    #[conf(default = 8080)]
    pub port: u16,  // Reads from APP_PORT, defaults to 8080
}

Invalid combinations (compile errors):

  • Option<T> + #[conf(default)] → Option already defaults to None
  • #[conf(deserializer = "...")] + #[conf(default)] → Not supported

Examples

See the examples/ directory for complete working examples:

Example Features Demonstrated
basic.rs Required fields, explicit default values
optional_fields.rs Option<T> for optional fields
default_trait.rs #[conf(default)] using Default trait
prefix.rs #[conf(prefix = "...")] at struct level
file_based_secrets.rs #[conf(from_file)] for Kubernetes/Docker secrets
custom_names.rs #[conf(name = "...")] for custom env var names
complex_types.rs Vec, HashMap with JSON deserializer
custom_deserialize_fn.rs Custom deserializer functions
comprehensive.rs Multiple features combined

Run with: cargo run --example <name>

Error Handling

match Config::from_env() {
    Ok(config) => println!("Config: {:?}", config),
    Err(e) => eprintln!("Error: {}", e),
}

Example errors:

  • Environment variable 'DATABASE_URL' is required but not set
  • Failed to parse environment variable 'PORT' as u16: invalid digit found in string
  • Failed to read file '/etc/secrets/key' for environment variable 'API_KEY_FILE': No such file or directory

Testing

cargo test

License

Licensed under MIT license (LICENSE or http://opensource.org/licenses/MIT).

Contribution

Contributions are welcome! Please feel free to submit a Pull Request.