Skip to content

Configuration Management, Secrets Lifecycle, and Multi-Environment Drift Control

Objective: Master production-grade configuration management across multi-environment distributed systems. When you need to prevent config drift, manage secrets securely, validate configurations, and maintain consistency across dev/stage/prod—this guide provides complete patterns and implementations.

Introduction

Configuration management is the foundation of reliable distributed systems. Misconfigured systems fail silently, leak secrets, and create cascading failures. This guide provides a complete framework for managing configuration across databases, clusters, services, and pipelines.

What This Guide Covers: - Configuration taxonomy and lifecycle - Secrets management and rotation - Multi-environment promotion workflows - Drift detection and prevention - Configuration validation patterns - Air-gapped environment strategies - Application-level configuration patterns - Monitoring and observability

Prerequisites: - Understanding of Kubernetes, Docker, and distributed systems - Familiarity with GitOps, Helm, and configuration tools - Experience with secrets management and encryption

Why Configuration Management Is Mission-Critical

The Cost of Configuration Failures

Postgres/Redis Misconfig → Outages: - Wrong connection pool settings cause connection exhaustion - Incorrect replication config leads to data loss - Memory limits too low cause OOM kills - Without proper config management, these failures cascade

ML Models Loading Wrong Paths → Silent Errors: - Model endpoints pointing to wrong buckets - Feature store paths misconfigured - Inference services using stale model versions - Results appear correct but are actually wrong

Tile Pipelines Pointing at Wrong Buckets → Broken Maps: - Geospatial tile generation using dev buckets in prod - Basemap URLs misconfigured - Vector tile endpoints incorrect - Maps fail to render or show wrong data

Prefect Pipelines Using Wrong Endpoints → Corrupted ETL: - ETL jobs writing to wrong databases - Data transformation pipelines using incorrect schemas - Backup jobs pointing to wrong object stores - Data corruption goes undetected until too late

Drift Between Dev/Stage/Prod → Impossible-to-Debug Behavior: - Configs work in dev but fail in prod - Environment-specific bugs that can't be reproduced - Different behavior across environments - Debugging becomes guesswork

Secrets Mismanagement → Catastrophic Leakage: - Hardcoded credentials in code - Secrets in environment variables exposed in logs - Rotated secrets not propagated - Full system compromise

Air-Gap Configs → Prone to Rot and Divergence: - Manual config updates lead to drift - No automated sync from connected environments - Configs become stale and incorrect - Recovery becomes impossible

K8s values.yaml Fragmentation → Environment Chaos: - Different values.yaml per environment - No single source of truth - Changes applied inconsistently - Rollouts fail unpredictably

Configuration Flow Through Systems

graph TB
    subgraph Source["Configuration Source"]
        Git["Git Repository<br/>(Helm, Kustomize, Configs)"]
        Vault["Vault/SOPS<br/>(Secrets)"]
        CI["CI/CD Pipeline<br/>(Validation)"]
    end

    subgraph GitOps["GitOps Layer"]
        ArgoCD["ArgoCD<br/>(Sync & Drift Detection)"]
        Flux["FluxCD<br/>(Continuous Sync)"]
    end

    subgraph K8s["Kubernetes Cluster"]
        ConfigMap["ConfigMaps"]
        Secret["Secrets<br/>(Encrypted at Rest)"]
        Helm["Helm Releases"]
    end

    subgraph Apps["Applications"]
        API["FastAPI/NiceGUI<br/>(Pydantic Config)"]
        ML["MLflow/ONNX<br/>(Model Configs)"]
        ETL["Prefect<br/>(Pipeline Configs)"]
        GIS["Geospatial<br/>(Tile Configs)"]
    end

    subgraph Data["Data Layer"]
        PG["Postgres<br/>(postgresql.conf)"]
        Redis["Redis<br/>(redis.conf)"]
        NGINX["NGINX<br/>(nginx.conf)"]
        ObjectStore["MinIO/S3<br/>(Bucket Policies)"]
    end

    Git --> CI
    CI --> GitOps
    Vault --> GitOps
    GitOps --> ArgoCD
    ArgoCD --> K8s
    K8s --> ConfigMap
    K8s --> Secret
    K8s --> Helm
    ConfigMap --> Apps
    Secret --> Apps
    Helm --> Apps
    Apps --> Data

    style Source fill:#e1f5ff
    style GitOps fill:#fff4e1
    style K8s fill:#e8f5e9
    style Apps fill:#f3e5f5
    style Data fill:#ffebee

The Golden Rules of Configuration Management

Rule 1: Configuration Must Be Declarative, Not Imperative

Bad (Imperative):

# Don't do this
kubectl set env deployment/api DATABASE_URL=$DB_URL
kubectl patch configmap app-config --type merge -p '{"data":{"key":"value"}}'

Good (Declarative):

# Do this - version-controlled YAML
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
data:
  database_url: "postgresql://db:5432/mydb"

Rule 2: Configuration Must Be Version-Controlled

  • All configuration in Git
  • Every change tracked and reviewed
  • Rollback via Git history
  • No manual cluster edits

Rule 3: No Configuration May Live Only in One Engineer's Head

  • Document all configuration decisions
  • Use ADRs for major config changes
  • Configuration must be self-documenting
  • Onboarding docs must include config setup

Rule 4: Secrets and Config Must Have Lifecycle Policies

  • Secrets must expire
  • Configs must be validated
  • Rotation schedules defined
  • Deprecation policies documented

Rule 5: Configs Must Be Validated Before Deployment

  • Schema validation
  • Pre-deploy CI checks
  • Automated testing
  • No invalid configs in production

Rule 6: Environment Config Drift Is Forbidden Unless Intentional and Documented

  • Automated drift detection
  • Drift alerts
  • Required approvals for intentional drift
  • Documentation of all drift exemptions

Rule 7: Config Changes Must Be Observable and Audit-Logged

  • All config changes logged
  • Config version tracking
  • Change attribution
  • Rollback capability

Types of Configuration and How to Handle Each

Static Configuration

Definition: Build-time configs, immutable container settings, typed structures.

Examples:

Pydantic Config (Python):

# config/static_config.py
from pydantic import BaseSettings, Field
from typing import Literal

class DatabaseConfig(BaseSettings):
    """Database connection configuration"""
    host: str = Field(..., description="Database host")
    port: int = Field(5432, description="Database port")
    database: str = Field(..., description="Database name")
    pool_size: int = Field(10, ge=1, le=100, description="Connection pool size")
    pool_timeout: int = Field(30, ge=1, description="Pool timeout in seconds")

    class Config:
        env_prefix = "DB_"
        case_sensitive = False

class AppConfig(BaseSettings):
    """Application configuration"""
    environment: Literal["dev", "stage", "prod"] = Field(..., description="Environment")
    log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field("INFO")
    api_timeout: int = Field(30, ge=1, description="API timeout in seconds")

    database: DatabaseConfig = Field(..., description="Database config")

    class Config:
        env_prefix = "APP_"
        case_sensitive = False

# Usage
config = AppConfig()

Go Struct Config:

// config/config.go
package config

import (
    "time"
    "github.com/kelseyhightower/envconfig"
)

type Config struct {
    Environment string `envconfig:"ENVIRONMENT" required:"true"`
    LogLevel    string `envconfig:"LOG_LEVEL" default:"INFO"`
    APITimeout  int    `envconfig:"API_TIMEOUT" default:"30"`

    Database DatabaseConfig `envconfig:"DB"`
}

type DatabaseConfig struct {
    Host      string        `envconfig:"HOST" required:"true"`
    Port      int           `envconfig:"PORT" default:"5432"`
    Database  string        `envconfig:"DATABASE" required:"true"`
    PoolSize  int           `envconfig:"POOL_SIZE" default:"10"`
    PoolTimeout time.Duration `envconfig:"POOL_TIMEOUT" default:"30s"`
}

func Load() (*Config, error) {
    var cfg Config
    err := envconfig.Process("", &cfg)
    return &cfg, err
}

Rust Serde Config:

// config/config.rs
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
    pub environment: String,
    #[serde(default = "default_log_level")]
    pub log_level: String,
    #[serde(default = "default_api_timeout")]
    pub api_timeout: u64,
    pub database: DatabaseConfig,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct DatabaseConfig {
    pub host: String,
    #[serde(default = "default_port")]
    pub port: u16,
    pub database: String,
    #[serde(default = "default_pool_size")]
    pub pool_size: u32,
}

fn default_log_level() -> String {
    "INFO".to_string()
}

fn default_api_timeout() -> u64 {
    30
}

fn default_port() -> u16 {
    5432
}

fn default_pool_size() -> u32 {
    10
}

Dynamic Configuration

Definition: Runtime parameters, feature flags, performance tuning.

Examples:

Redis-Based Feature Flags:

# config/feature_flags.py
import redis
import json

class FeatureFlags:
    def __init__(self, redis_client: redis.Redis):
        self.redis = redis_client

    def is_enabled(self, flag: str, user_id: str = None) -> bool:
        """Check if feature flag is enabled"""
        key = f"feature_flag:{flag}"

        # Check global flag
        global_value = self.redis.get(key)
        if global_value:
            config = json.loads(global_value)
            if config.get("enabled", False):
                return True

        # Check user-specific flag
        if user_id:
            user_key = f"feature_flag:{flag}:{user_id}"
            user_value = self.redis.get(user_key)
            if user_value:
                return json.loads(user_value).get("enabled", False)

        return False

    def set_flag(self, flag: str, enabled: bool, user_id: str = None):
        """Set feature flag"""
        key = f"feature_flag:{flag}" if not user_id else f"feature_flag:{flag}:{user_id}"
        self.redis.set(
            key,
            json.dumps({"enabled": enabled, "updated_at": datetime.now().isoformat()}),
            ex=86400  # 24 hour TTL
        )

ConfigMap-Based Dynamic Config:

# k8s/dynamic-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dynamic-config
data:
  feature_flags.json: |
    {
      "enable_new_api": true,
      "enable_ml_inference": false,
      "cache_ttl_seconds": 300
    }
  performance_tuning.json: |
    {
      "max_concurrent_requests": 100,
      "request_timeout_ms": 5000,
      "retry_attempts": 3
    }

Secrets

Definition: API keys, database passwords, encryption keys, OAuth/OIDC credentials.

Examples:

External Secrets Operator:

# k8s/external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: database-credentials
  namespace: app
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: vault-backend
    kind: SecretStore
  target:
    name: database-secret
    creationPolicy: Owner
  data:
    - secretKey: password
      remoteRef:
        key: database/prod
        property: password
    - secretKey: username
      remoteRef:
        key: database/prod
        property: username

SOPS Encrypted Config:

# config/secrets.enc.yaml
# Encrypted with SOPS
database:
    password: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]
    api_key: ENC[AES256_GCM,data:...,iv:...,tag:...,type:str]

Sensitive Configs

Definition: PII access controls, ML model endpoints, geospatial tile endpoints.

Examples:

ML Model Endpoint Config:

# config/ml_config.py
from pydantic import BaseSettings, SecretStr

class MLConfig(BaseSettings):
    """ML model configuration"""
    model_endpoint: str = Field(..., description="ONNX model endpoint")
    feature_store_endpoint: str = Field(..., description="Feature store endpoint")
    inference_timeout: int = Field(5, ge=1, description="Inference timeout")
    batch_size: int = Field(32, ge=1, le=128, description="Batch size")

    # Sensitive - use SecretStr
    api_key: SecretStr = Field(..., description="ML API key")

    class Config:
        env_prefix = "ML_"

Geospatial Tile Config:

# config/gis_config.py
class GISConfig(BaseSettings):
    """Geospatial configuration"""
    tile_endpoint: str = Field(..., description="Vector tile endpoint")
    basemap_url: str = Field(..., description="Basemap URL")
    max_zoom: int = Field(12, ge=1, le=18, description="Maximum zoom level")
    cache_ttl: int = Field(3600, ge=0, description="Cache TTL in seconds")

    # Sensitive bucket paths
    tile_bucket: str = Field(..., description="Tile storage bucket")
    data_bucket: str = Field(..., description="Geospatial data bucket")

    class Config:
        env_prefix = "GIS_"

Environment-Level Configs

Definition: Cluster URLs, bucket paths, MLflow tracking URIs, Redis endpoints.

Examples:

Environment-Specific Helm Values:

# helm/values-dev.yaml
environment: dev
cluster:
  name: dev-cluster
  endpoint: https://dev-k8s.example.com

databases:
  postgres:
    host: postgres-dev.example.com
    port: 5432
  redis:
    host: redis-dev.example.com
    port: 6379

object_stores:
  s3:
    endpoint: https://minio-dev.example.com
    bucket: raw-data-dev
    prefix: dev/

mlflow:
  tracking_uri: http://mlflow-dev.example.com:5000

# helm/values-prod.yaml
environment: prod
cluster:
  name: prod-cluster
  endpoint: https://prod-k8s.example.com

databases:
  postgres:
    host: postgres-prod.example.com
    port: 5432
  redis:
    host: redis-prod.example.com
    port: 6379

object_stores:
  s3:
    endpoint: https://s3-prod.example.com
    bucket: raw-data-prod
    prefix: prod/

mlflow:
  tracking_uri: http://mlflow-prod.example.com:5000

GitOps (ArgoCD, FluxCD)

ArgoCD Application:

# argocd/app.yaml
apiVersion: argocd.applications/v1alpha1
kind: Application
metadata:
  name: app-prod
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/myorg/configs.git
    targetRevision: main
    path: k8s/app
    helm:
      valueFiles:
        - values-prod.yaml
  destination:
    server: https://kubernetes.default.svc
    namespace: app-prod
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

FluxCD Kustomization:

# flux/kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: app-prod
  namespace: flux-system
spec:
  interval: 5m
  path: ./k8s/app
  prune: true
  sourceRef:
    kind: GitRepository
    name: configs
  validation: client
  postBuild:
    substitute:
      ENVIRONMENT: prod
      CLUSTER_NAME: prod-cluster

Helm and Kustomize

Helm Chart Structure:

helm/app/
├── Chart.yaml
├── values.yaml
├── values-dev.yaml
├── values-stage.yaml
├── values-prod.yaml
└── templates/
    ├── deployment.yaml
    ├── configmap.yaml
    └── secret.yaml

Kustomize Overlay:

# kustomize/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - configmap.yaml

# kustomize/overlays/prod/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - ../../base
patches:
  - path: configmap-patch.yaml
  - path: deployment-patch.yaml
configMapGenerator:
  - name: app-config
    behavior: merge
    literals:
      - ENVIRONMENT=prod
      - LOG_LEVEL=INFO

SOPS + age

SOPS Configuration:

# .sops.yaml
creation_rules:
  - path_regex: secrets/.*\.yaml$
    age: >-
      age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
    encrypted_regex: ^(data|stringData|password|api_key|secret)$

Encrypting Secrets:

# Encrypt file
sops -e -i secrets/database.yaml

# Edit encrypted file
sops secrets/database.yaml

# Decrypt for CI/CD
sops -d secrets/database.yaml | kubectl apply -f -

External Secrets Operator

SecretStore:

# k8s/secret-store.yaml
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: vault-backend
  namespace: app
spec:
  provider:
    vault:
      server: https://vault.example.com
      path: secret
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes
          role: app-role
          serviceAccountRef:
            name: external-secrets

Configuration Validation

Conftest + OPA:

# policies/config.rego
package config

deny[msg] {
    input.kind == "ConfigMap"
    input.data.environment == "prod"
    not input.data.log_level
    msg := "Production configs must specify log_level"
}

deny[msg] {
    input.kind == "ConfigMap"
    input.data.environment == "prod"
    input.data.log_level == "DEBUG"
    msg := "Production configs cannot use DEBUG log level"
}

Validation in CI:

# .github/workflows/validate-config.yaml
name: Validate Configuration
on:
  pull_request:
    paths:
      - 'k8s/**'
      - 'helm/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Validate Helm charts
        run: |
          helm lint helm/app
          helm template helm/app | kubeconform -strict

      - name: Validate with Conftest
        run: |
          helm template helm/app | conftest test -

      - name: Validate Kustomize
        run: |
          kustomize build k8s/app | kubeconform -strict

Schema-Based Validation

Pydantic Schema:

# config/schema.py
from pydantic import BaseModel, validator
from typing import Literal

class ConfigSchema(BaseModel):
    environment: Literal["dev", "stage", "prod"]
    log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]
    api_timeout: int

    @validator('api_timeout')
    def validate_timeout(cls, v):
        if v < 1 or v > 300:
            raise ValueError('api_timeout must be between 1 and 300')
        return v

JSON Schema Validation:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "environment": {
      "type": "string",
      "enum": ["dev", "stage", "prod"]
    },
    "log_level": {
      "type": "string",
      "enum": ["DEBUG", "INFO", "WARNING", "ERROR"]
    },
    "api_timeout": {
      "type": "integer",
      "minimum": 1,
      "maximum": 300
    }
  },
  "required": ["environment", "log_level", "api_timeout"]
}

Environment Promotion Workflow

Dev → Stage → Prod Promotion

graph LR
    Dev["Dev Environment<br/>(Automated)"]
    Stage["Stage Environment<br/>(Automated + Approval)"]
    Prod["Prod Environment<br/>(Manual Approval)"]

    Dev -->|"Validate & Test"| Validate["Config Validation"]
    Validate -->|"Promote"| Stage
    Stage -->|"Validate & Test"| Validate2["Config Validation"]
    Validate2 -->|"Approve"| Approval["Manual Approval"]
    Approval -->|"Promote"| Prod

    style Dev fill:#e8f5e9
    style Stage fill:#fff4e1
    style Prod fill:#ffebee

Promotion Script:

# scripts/promote_config.py
import subprocess
import sys
from pathlib import Path

def promote_config(source_env: str, target_env: str, config_path: Path):
    """Promote configuration from source to target environment"""
    # 1. Validate source config
    print(f"Validating {source_env} configuration...")
    if not validate_config(config_path / f"values-{source_env}.yaml"):
        print(f"ERROR: {source_env} config validation failed")
        sys.exit(1)

    # 2. Check for drift
    print(f"Checking for drift in {target_env}...")
    drift = check_drift(source_env, target_env)
    if drift:
        print(f"WARNING: Drift detected between {source_env} and {target_env}")
        if not confirm("Continue with promotion?"):
            sys.exit(1)

    # 3. Create promotion PR
    print(f"Creating promotion PR from {source_env} to {target_env}...")
    pr_url = create_promotion_pr(source_env, target_env, config_path)
    print(f"Promotion PR: {pr_url}")

    # 4. Run validation in target environment
    print(f"Running validation in {target_env}...")
    if not validate_in_target(target_env):
        print(f"ERROR: Validation failed in {target_env}")
        sys.exit(1)

    # 5. Apply config (if auto-approve)
    if target_env != "prod":
        print(f"Applying configuration to {target_env}...")
        apply_config(target_env, config_path)
    else:
        print("Production promotion requires manual approval")
        print(f"Review PR: {pr_url}")

def validate_config(config_file: Path) -> bool:
    """Validate configuration file"""
    result = subprocess.run(
        ["helm", "lint", "-f", str(config_file)],
        capture_output=True
    )
    return result.returncode == 0

def check_drift(source_env: str, target_env: str) -> bool:
    """Check for configuration drift"""
    # Compare configs
    source_config = load_config(f"values-{source_env}.yaml")
    target_config = load_config(f"values-{target_env}.yaml")

    return source_config != target_config

Automated Release Tagging

# .github/workflows/promote-config.yaml
name: Promote Configuration
on:
  workflow_dispatch:
    inputs:
      source_env:
        description: 'Source environment'
        required: true
        type: choice
        options:
          - dev
          - stage
      target_env:
        description: 'Target environment'
        required: true
        type: choice
        options:
          - stage
          - prod

jobs:
  promote:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Validate source config
        run: |
          helm lint -f helm/app/values-${{ github.event.inputs.source_env }}.yaml

      - name: Check drift
        run: |
          python scripts/check_drift.py \
            --source ${{ github.event.inputs.source_env }} \
            --target ${{ github.event.inputs.target_env }}

      - name: Create promotion PR
        run: |
          gh pr create \
            --title "Promote config: ${{ github.event.inputs.source_env }} → ${{ github.event.inputs.target_env }}" \
            --body "Automated promotion from ${{ github.event.inputs.source_env }} to ${{ github.event.inputs.target_env }}"

      - name: Tag release
        if: github.event.inputs.target_env == 'prod'
        run: |
          git tag -a "config-v$(date +%Y%m%d-%H%M%S)" -m "Config promotion to prod"
          git push origin --tags

Multi-Environment Drift Prevention

Drift Detection Tools

kubectl diff:

# Compare desired vs actual
kubectl diff -f k8s/app/ | tee drift-report.txt

# Check specific resource
kubectl diff -f k8s/app/deployment.yaml

ArgoCD Drift Detection:

# argocd/app-with-drift-detection.yaml
apiVersion: argocd.applications/v1alpha1
kind: Application
metadata:
  name: app-prod
spec:
  syncPolicy:
    automated:
      prune: true
      selfHeal: true  # Auto-correct drift
    syncOptions:
      - CreateNamespace=true
  # ArgoCD automatically detects and reports drift

Postgres Schema Comparison:

# scripts/detect_postgres_drift.py
import psycopg2
from schema_diff import compare_schemas

def detect_postgres_drift(dev_conn, prod_conn):
    """Detect schema drift between dev and prod"""
    dev_schema = get_schema(dev_conn)
    prod_schema = get_schema(prod_conn)

    diff = compare_schemas(dev_schema, prod_schema)

    if diff:
        print("Schema drift detected:")
        for change in diff:
            print(f"  - {change}")
        return True

    return False

def get_schema(conn):
    """Get database schema"""
    with conn.cursor() as cur:
        cur.execute("""
            SELECT 
                schemaname,
                tablename,
                columnname,
                datatype
            FROM pg_catalog.pg_table_def
            WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
            ORDER BY schemaname, tablename, columnname
        """)
        return cur.fetchall()

Redis Config Audit:

# scripts/detect_redis_drift.py
import redis

def detect_redis_drift(dev_client: redis.Redis, prod_client: redis.Redis):
    """Detect Redis configuration drift"""
    dev_config = dev_client.config_get()
    prod_config = prod_client.config_get()

    drift = []
    for key in set(dev_config.keys()) | set(prod_config.keys()):
        if dev_config.get(key) != prod_config.get(key):
            drift.append({
                "key": key,
                "dev": dev_config.get(key),
                "prod": prod_config.get(key)
            })

    return drift

NGINX Config Hash Diffing:

# scripts/detect_nginx_drift.sh
#!/bin/bash

DEV_CONFIG="nginx/nginx-dev.conf"
PROD_CONFIG="nginx/nginx-prod.conf"

DEV_HASH=$(md5sum "$DEV_CONFIG" | cut -d' ' -f1)
PROD_HASH=$(md5sum "$PROD_CONFIG" | cut -d' ' -f1)

if [ "$DEV_HASH" != "$PROD_HASH" ]; then
    echo "NGINX config drift detected"
    diff -u "$DEV_CONFIG" "$PROD_CONFIG"
    exit 1
fi

Automated Nightly Drift Detection:

# .github/workflows/nightly-drift-check.yaml
name: Nightly Drift Detection
on:
  schedule:
    - cron: '0 2 * * *'  # 2 AM daily
  workflow_dispatch:

jobs:
  detect-drift:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Check K8s drift
        run: |
          kubectl diff -f k8s/app/ > drift-report.txt
          if [ -s drift-report.txt ]; then
            echo "Drift detected!"
            cat drift-report.txt
            exit 1
          fi

      - name: Check Postgres drift
        run: |
          python scripts/detect_postgres_drift.py

      - name: Check Redis drift
        run: |
          python scripts/detect_redis_drift.py

      - name: Report drift
        if: failure()
        run: |
          gh issue create \
            --title "Configuration drift detected" \
            --body "$(cat drift-report.txt)"

Secrets Lifecycle & Rotation Policy

Secret Creation

Secret Creation Workflow:

# scripts/create_secret.py
from cryptography.fernet import Fernet
import secrets
import json

class SecretManager:
    def __init__(self, vault_client):
        self.vault = vault_client

    def create_secret(
        self,
        secret_name: str,
        secret_type: str,
        environment: str,
        scopes: list,
        expires_days: int = 90
    ) -> dict:
        """Create new secret with lifecycle policy"""
        # Generate secret value
        if secret_type == "api_key":
            value = secrets.token_urlsafe(32)
        elif secret_type == "password":
            value = secrets.token_urlsafe(24)
        elif secret_type == "jwt_signing_key":
            value = Fernet.generate_key().decode()
        else:
            raise ValueError(f"Unknown secret type: {secret_type}")

        # Store in Vault
        self.vault.write(
            f"secret/{environment}/{secret_name}",
            value=value,
            type=secret_type,
            scopes=scopes,
            created_at=datetime.now().isoformat(),
            expires_at=(datetime.now() + timedelta(days=expires_days)).isoformat()
        )

        return {
            "name": secret_name,
            "value": value,  # Only returned once
            "expires_at": (datetime.now() + timedelta(days=expires_days)).isoformat()
        }

Automated Rotation

Database Password Rotation:

# scripts/rotate_db_password.py
import asyncpg
from vault_client import VaultClient

class DatabasePasswordRotator:
    def __init__(self, vault: VaultClient, db_pool: asyncpg.Pool):
        self.vault = vault
        self.db_pool = db_pool

    async def rotate_password(self, user: str, environment: str):
        """Rotate database password with zero downtime"""
        # 1. Generate new password
        new_password = secrets.token_urlsafe(24)

        # 2. Create new user with new password (dual-secret pattern)
        new_user = f"{user}_new"
        async with self.db_pool.acquire() as conn:
            await conn.execute(f"""
                CREATE USER {new_user} WITH PASSWORD %s;
                GRANT ALL PRIVILEGES ON DATABASE mydb TO {new_user};
            """, new_password)

        # 3. Update application config (grace period)
        await self.update_app_config(new_user, new_password, grace_period=True)

        # 4. Wait for connections to migrate
        await asyncio.sleep(300)  # 5 minute grace period

        # 5. Drop old user
        async with self.db_pool.acquire() as conn:
            await conn.execute(f"DROP USER {user}")

        # 6. Rename new user
        async with self.db_pool.acquire() as conn:
            await conn.execute(f"ALTER USER {new_user} RENAME TO {user}")

        # 7. Update Vault
        self.vault.write(
            f"secret/{environment}/database/{user}",
            password=new_password,
            rotated_at=datetime.now().isoformat()
        )

API Key Rotation:

# k8s/api-key-rotation.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
  name: rotate-api-keys
spec:
  schedule: "0 0 * * 0"  # Weekly
  jobTemplate:
    spec:
      template:
        spec:
          containers:
            - name: rotate-keys
              image: secret-rotator:latest
              command:
                - python
                - /app/rotate_api_keys.py
                - --environment
                - prod
                - --rotation-interval
                - "90d"

Secret Retirement

# scripts/retire_secret.py
class SecretRetirer:
    def __init__(self, vault: VaultClient):
        self.vault = vault

    def retire_secret(self, secret_name: str, environment: str, reason: str):
        """Retire and archive secret"""
        # 1. Mark as retired
        secret_data = self.vault.read(f"secret/{environment}/{secret_name}")
        secret_data["retired"] = True
        secret_data["retired_at"] = datetime.now().isoformat()
        secret_data["retirement_reason"] = reason

        # 2. Move to archive
        self.vault.write(
            f"archive/{environment}/{secret_name}",
            **secret_data
        )

        # 3. Delete from active secrets
        self.vault.delete(f"secret/{environment}/{secret_name}")

        # 4. Revoke all tokens using this secret
        self.revoke_tokens(secret_name, environment)

Configuration Validation Patterns

Pre-Deploy CI Validation

# .github/workflows/validate-config.yaml
name: Validate Configuration
on:
  pull_request:
    paths:
      - 'k8s/**'
      - 'helm/**'
      - 'config/**'

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Validate Helm
        run: |
          helm lint helm/app
          helm template helm/app | kubeconform -strict

      - name: Validate with Conftest
        run: |
          helm template helm/app | conftest test -p policies/

      - name: Validate Pydantic schemas
        run: |
          python -m pytest tests/test_config_schema.py

      - name: Validate JSON schemas
        run: |
          ajv validate -s config/schema.json -d config/values-dev.json

Schema Enforcement

Pydantic Config Validation:

# config/validator.py
from pydantic import BaseModel, ValidationError
import yaml

def validate_config_file(config_path: Path, schema: BaseModel):
    """Validate configuration file against schema"""
    with open(config_path) as f:
        config_data = yaml.safe_load(f)

    try:
        validated = schema(**config_data)
        return validated
    except ValidationError as e:
        print(f"Validation errors in {config_path}:")
        for error in e.errors():
            print(f"  - {error['loc']}: {error['msg']}")
        raise

Configuration for Air-Gapped Environments

Bundling Encrypted Config Packages

# scripts/bundle_airgap_config.sh
#!/bin/bash

BUNDLE_NAME="config-bundle-$(date +%Y%m%d).tar.gz"
ENCRYPTION_KEY="age1xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

# 1. Bundle configs
tar -czf config-bundle.tar.gz \
    k8s/ \
    helm/ \
    config/ \
    secrets.enc.yaml

# 2. Encrypt bundle
sops -e --age "$ENCRYPTION_KEY" config-bundle.tar.gz > "$BUNDLE_NAME"

# 3. Create manifest
cat > manifest.json <<EOF
{
  "bundle_name": "$BUNDLE_NAME",
  "created_at": "$(date -Iseconds)",
  "config_version": "$(git rev-parse HEAD)",
  "checksum": "$(sha256sum $BUNDLE_NAME | cut -d' ' -f1)"
}
EOF

# 4. Sign manifest
gpg --sign manifest.json

Offline ArgoCD Sync

# argocd/airgap-sync.yaml
apiVersion: argocd.applications/v1alpha1
kind: Application
metadata:
  name: app-airgap
spec:
  source:
    repoURL: file:///mnt/config-bundle  # Local file system
    path: k8s/app
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true

Application-Level Configuration Strategy

NiceGUI Config Loading

# nicegui/config.py
from nicegui import app
from pydantic import BaseSettings
import os

class NiceGUIConfig(BaseSettings):
    """NiceGUI application configuration"""
    title: str = "My App"
    dark_mode: bool = True
    api_endpoint: str
    database_url: str
    redis_url: str

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

# Load config
config = NiceGUIConfig()

# Use in app
app.config = config

FastAPI Config

# fastapi/config.py
from fastapi import FastAPI
from pydantic import BaseSettings

class Settings(BaseSettings):
    app_name: str = "My API"
    database_url: str
    redis_url: str
    api_key: str

    class Config:
        env_file = ".env"

settings = Settings()

app = FastAPI(title=settings.app_name)

@app.get("/health")
async def health():
    return {"status": "healthy", "config_loaded": True}

MLflow Config Binding

# mlflow/config.py
import mlflow
from pydantic import BaseSettings

class MLflowConfig(BaseSettings):
    tracking_uri: str
    experiment_name: str = "default"
    registry_uri: str = None

    class Config:
        env_prefix = "MLFLOW_"

config = MLflowConfig()

# Configure MLflow
mlflow.set_tracking_uri(config.tracking_uri)
mlflow.set_experiment(config.experiment_name)

Configuration Anti-Patterns

1. "Configs Live in Slack Messages"

Symptom: Configuration shared via chat, not version-controlled.

Fix: All configs in Git, use PRs for changes.

2. Hard-Coded Credentials in Scripts

Symptom: Passwords and keys in source code.

Fix: Use secrets management, environment variables, or Vault.

3. .env Files Pushed to Git

Symptom: .env files committed to repository.

Fix: Use .env.example, add .env to .gitignore, use SOPS for encrypted secrets.

4. K8s Secrets Stored Unencrypted

Symptom: Kubernetes secrets in plain YAML.

Fix: Use External Secrets Operator, SOPS, or sealed-secrets.

5. Configs Duplicated Across Repos

Symptom: Same config in multiple repositories.

Fix: Central config repository, use Git submodules or config packages.

6. Environment-Specific values.yaml Files Scattered

Symptom: values.yaml files in multiple locations.

Fix: Single Helm chart with environment overlays.

7. Missing Config Validation

Symptom: Invalid configs deployed to production.

Fix: Pre-deploy validation, schema enforcement, CI checks.

8. Runtime Mutation of Config

Symptom: Configs changed at runtime, causing nondeterminism.

Fix: Immutable configs, require restart for changes.

9. No Defined Secret Rotation

Symptom: Secrets never rotated, high risk of compromise.

Fix: Automated rotation schedules, expiration policies.

10. Configs Differ Between Nodes Without Reason

Symptom: Inconsistent configs across cluster nodes.

Fix: Centralized config management, GitOps, automated sync.

11. Manual Cluster Config Updates

Symptom: kubectl edit, manual ConfigMap changes.

Fix: GitOps workflow, all changes via Git.

12. Raw YAML Copy/Paste Without Templating

Symptom: Duplicated YAML with hardcoded values.

Fix: Helm/Kustomize templating, environment variables.

Testing Configuration Changes

Unit Testing Configuration Parsers

# tests/test_config.py
import pytest
from config import AppConfig, DatabaseConfig

def test_config_loading():
    """Test configuration loading"""
    config = AppConfig(
        environment="dev",
        log_level="INFO",
        database=DatabaseConfig(
            host="localhost",
            port=5432,
            database="testdb"
        )
    )

    assert config.environment == "dev"
    assert config.database.host == "localhost"

def test_config_validation():
    """Test configuration validation"""
    with pytest.raises(ValueError):
        AppConfig(
            environment="invalid",
            log_level="INFO"
        )

Integration Tests

# tests/test_config_integration.py
@pytest.mark.integration
def test_config_deployment():
    """Test configuration deployment"""
    # Deploy config to test cluster
    subprocess.run(["kubectl", "apply", "-f", "k8s/test/"])

    # Verify config loaded
    result = subprocess.run(
        ["kubectl", "get", "configmap", "app-config"],
        capture_output=True
    )
    assert result.returncode == 0

Monitoring & Observability of Config

Prometheus Metrics

# monitoring/config_metrics.py
from prometheus_client import Counter, Gauge, Histogram

config_load_errors = Counter(
    'config_load_errors_total',
    'Total config load errors',
    ['environment', 'config_type']
)

config_drift_detected = Gauge(
    'config_drift_detected',
    'Configuration drift detected',
    ['environment']
)

secret_expiration_days = Gauge(
    'secret_expiration_days',
    'Days until secret expiration',
    ['secret_name', 'environment']
)

PromQL Queries

# Config load errors
rate(config_load_errors_total[5m])

# Secrets nearing expiration
secret_expiration_days < 7

# Config drift
config_drift_detected > 0

Config Governance Checklists

Daily Checklist

  • Review config change PRs
  • Check for drift alerts
  • Verify secret expiration warnings
  • Monitor config load errors

Weekly Config Audit

  • Review all config changes
  • Check for anti-patterns
  • Verify environment consistency
  • Update documentation

Monthly Secrets Rotation

  • Rotate database passwords
  • Rotate API keys
  • Rotate TLS certificates
  • Update rotation schedules

Quarterly Drift Review

  • Compare dev/stage/prod configs
  • Document intentional drift
  • Resolve unintentional drift
  • Update drift policies

Release Checklist

  • Validate all configs
  • Check secret expiration
  • Verify environment promotion
  • Test rollback procedure
  • Update documentation

DR Test Checklist

  • Test config restore
  • Verify secret recovery
  • Test air-gap config loading
  • Validate disaster recovery procedures

See Also


This guide provides a complete framework for configuration management. Start with version control, add validation, implement GitOps, and monitor for drift. The goal is consistency, reliability, and security across all environments.