Skip to content

Latest commit

 

History

31 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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 = "0.2"
# Or from GitHub
#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() {
    let config = Config::from_env().unwrap();
    println!("Port: {}", config.port);
}

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)] or #[conf(default = value)] → Option already defaults to None

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.

About

Environment variable configuration with file-based secrets support

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages