Cross-Environment Configuration Strategy, Drift Prevention, and Multi-Cluster State Management: Best Practices¶
Introduction¶
Configuration governance is the foundation of reliable, reproducible distributed systems. Without proper configuration management, environments drift, deployments fail, and systems become unpredictable. This guide provides a complete framework for managing configuration across all environments and deployment targets.
What This Guide Covers: - The problem space: why configuration governance is critical - Core principles of environment configuration strategy - Patterns for multi-environment configuration design - Anti-patterns and real-world pitfalls - Environment promotion pipelines - Automated drift detection and enforcement - Configuration metadata and versioning - Secrets lifecycle governance - Tooling and implementation examples - Agentic LLM integration for configuration
Prerequisites: - Understanding of distributed systems and Kubernetes - Familiarity with configuration management and GitOps - Experience with multi-environment deployments
The Problem Space: Why Config Governance Is Critical¶
Configuration Drift Types¶
Schema Drift: Database schemas diverge between environments.
Example:
-- Dev: Has new column
ALTER TABLE users ADD COLUMN email_verified BOOLEAN;
-- Prod: Missing column
-- Schema drift: dev has email_verified, prod doesn't
Impact: Application code works in dev, fails in prod.
Secrets Drift: Secrets differ between environments.
Example:
# Dev: Test credentials
database_password: "test123"
# Prod: Production credentials (different)
database_password: "prod-secret-xyz"
Impact: Security vulnerabilities, authentication failures.
Runtime Drift: Runtime configuration differs.
Example:
# Dev: Low resource limits
resources:
requests:
cpu: 100m
memory: 256Mi
# Prod: High resource limits
resources:
requests:
cpu: 1000m
memory: 2Gi
Impact: Performance differences, resource contention.
Cluster State Drift: Kubernetes cluster state diverges.
Example:
# Dev: 3 replicas
kubectl get deployment app -n dev
# replicas: 3
# Prod: 5 replicas (drifted)
kubectl get deployment app -n prod
# replicas: 5
Impact: Inconsistent scaling, unpredictable behavior.
API Contract Drift: API contracts differ between environments.
Example:
# Dev: API v2 with new field
class UserV2(BaseModel):
id: int
name: str
email: str # New in v2
# Prod: Still on API v1
class UserV1(BaseModel):
id: int
name: str
Impact: Integration failures, breaking changes.
Operator/Default Drift: Operator defaults change over time.
Example:
# Old operator version: default replicas = 1
# New operator version: default replicas = 3
# Deployments drift without explicit config
Impact: Unintended scaling, resource consumption.
Environment Variable Entropy: Environment variables become inconsistent.
Example:
# Dev
DATABASE_URL=postgresql://dev-db:5432/app
LOG_LEVEL=DEBUG
# Prod
DATABASE_URL=postgresql://prod-db:5432/app
LOG_LEVEL=INFO
# Missing: LOG_LEVEL in some pods
Impact: Inconsistent behavior, debugging difficulties.
Config Shadowing: Configs override each other unexpectedly.
Example:
# base.yaml
database:
host: "localhost"
port: 5432
# override.yaml
database:
host: "remote-host"
# port not specified, but might be overridden by env var
# Config shadowing: unclear which value wins
Impact: Unpredictable configuration resolution.
Version Skew Between Environments: Different versions deployed.
Example:
# Dev: Latest version
image: app:v1.5.0
# Prod: Older version
image: app:v1.3.0
# Version skew: features available in dev not in prod
Impact: Feature inconsistencies, testing gaps.
Consequences of Configuration Drift¶
Inconsistent Correctness Guarantees: - Dev works, prod fails - Different behavior across environments - Unpredictable outcomes
Broken Reproducibility: - Can't reproduce production issues in staging - Different configurations yield different results - Testing becomes unreliable
Unstable Deployments: - Deployments fail due to config mismatches - Rollbacks don't work because configs differ - Recovery procedures fail
Unpredictable Performance Behavior: - Different resource limits cause performance differences - Caching configurations differ - Load balancing behaves differently
Security Gaps: - Secrets drift creates vulnerabilities - Access controls differ - Audit trails incomplete
Misaligned Audit Trails: - Config changes not tracked - Drift not detected - Compliance violations
"Can't Reproduce in Staging" Anti-Patterns: - Issues only occur in production - Staging doesn't match production - Debugging becomes impossible
Core Principles of Environment Configuration Strategy¶
"Configuration as a Product"¶
Principle: Treat configuration as a first-class product with: - Versioning - Testing - Documentation - Lifecycle management
Example:
# config/product.yaml
apiVersion: v1
kind: ConfigProduct
metadata:
name: app-config
version: 1.2.3
spec:
environments:
- name: dev
version: 1.2.3-dev
- name: prod
version: 1.2.3
Declarative Configuration First¶
Principle: All configuration should be declarative, not imperative.
Example:
# Declarative: Desired state
apiVersion: apps/v1
kind: Deployment
spec:
replicas: 3
template:
spec:
containers:
- name: app
image: app:v1.2.3
# Not: kubectl scale deployment app --replicas=3
Immutable Infrastructure and Reproducible Builds¶
Principle: Infrastructure and builds should be immutable and reproducible.
Example:
# Immutable: Pinned base image
FROM python:3.11-slim@sha256:abc123...
# Immutable: Pinned dependencies
COPY requirements-lock.txt .
RUN pip install --no-cache-dir -r requirements-lock.txt
Fail-Fast Configuration Validation¶
Principle: Validate configuration early and fail fast.
Example:
# Fail-fast validation
from pydantic import BaseModel, ValidationError
class AppConfig(BaseModel):
database_url: str
redis_url: str
log_level: str
@validator('log_level')
def validate_log_level(cls, v):
if v not in ['DEBUG', 'INFO', 'WARNING', 'ERROR']:
raise ValueError(f"Invalid log level: {v}")
return v
# Validate at startup
try:
config = AppConfig(**load_config())
except ValidationError as e:
raise SystemExit(f"Configuration validation failed: {e}")
Explicit Environment Boundaries¶
Principle: Make environment boundaries explicit and enforced.
Example:
# Explicit environment boundaries
environments:
dev:
cluster: dev-cluster
namespace: dev
config_source: config/dev/
prod:
cluster: prod-cluster
namespace: prod
config_source: config/prod/
Thin Environment Differences (TED Principle)¶
Principle: Minimize differences between environments.
Example:
# Base config (shared)
base:
database:
pool_size: 10
timeout: 30
# Environment-specific overrides (thin)
dev:
database:
pool_size: 5 # Only difference
prod:
database:
pool_size: 20 # Only difference
Component Autonomy & Isolation¶
Principle: Components should be autonomous and isolated.
Example:
# Autonomous component config
components:
user-service:
config:
database_url: ${USER_DB_URL}
redis_url: ${REDIS_URL}
# No dependencies on other component configs
Configuration Layering¶
Principle: Layer configuration from base to runtime.
Layers: 1. Base: Common configuration 2. Environment: Environment-specific overrides 3. Secrets: Secret values 4. Runtime: Runtime-specific adjustments
Example:
# Layer 1: Base
base:
app:
name: myapp
version: 1.0.0
# Layer 2: Environment
environments:
dev:
app:
debug: true
prod:
app:
debug: false
# Layer 3: Secrets
secrets:
database_password: ${DB_PASSWORD}
# Layer 4: Runtime
runtime:
replicas: ${REPLICAS}
Version-Locked Everything¶
Principle: Lock versions of all dependencies.
Example:
# Version-locked config
versions:
app: "1.2.3"
postgres: "15.3"
redis: "7.2.0"
python: "3.11.5"
dependencies:
fastapi: "0.104.0"
pydantic: "2.5.0"
Patterns for Multi-Environment Configuration Design¶
Canonical Configuration Hierarchy¶
Structure:
config/
├── base/
│ ├── app.yaml
│ ├── database.yaml
│ └── redis.yaml
├── environments/
│ ├── dev/
│ │ ├── app.yaml
│ │ └── database.yaml
│ ├── staging/
│ │ ├── app.yaml
│ │ └── database.yaml
│ └── prod/
│ ├── app.yaml
│ └── database.yaml
└── secrets/
├── dev/
└── prod/
Resolution:
# config_resolver.py
class ConfigResolver:
def resolve(self, environment: str) -> dict:
"""Resolve configuration for environment"""
# Load base
base_config = load_config("config/base/")
# Override with environment
env_config = load_config(f"config/environments/{environment}/")
# Merge
config = deep_merge(base_config, env_config)
# Inject secrets
secrets = load_secrets(f"config/secrets/{environment}/")
config = inject_secrets(config, secrets)
return config
Dedicated Schema Repository and Environment Diffs¶
Schema Repository:
schemas/
├── postgres/
│ ├── v1/
│ │ └── schema.sql
│ └── v2/
│ └── schema.sql
├── api/
│ ├── v1/
│ │ └── openapi.yaml
│ └── v2/
│ └── openapi.yaml
└── config/
├── v1/
│ └── config-schema.json
└── v2/
└── config-schema.json
Environment Diffing:
# diff_tool.py
class EnvironmentDiffer:
def diff_environments(self, env1: str, env2: str) -> dict:
"""Diff configurations between environments"""
config1 = load_config(env1)
config2 = load_config(env2)
diff = {
'added': find_added_keys(config1, config2),
'removed': find_removed_keys(config1, config2),
'changed': find_changed_values(config1, config2)
}
return diff
Context-Aware Config Resolvers¶
Resolver:
# context_aware_resolver.py
class ContextAwareConfigResolver:
def resolve(self, context: dict) -> dict:
"""Resolve config based on context"""
environment = context.get('environment')
cluster = context.get('cluster')
region = context.get('region')
# Load base
config = load_base_config()
# Apply environment overrides
config = apply_environment_overrides(config, environment)
# Apply cluster overrides
config = apply_cluster_overrides(config, cluster)
# Apply region overrides
config = apply_region_overrides(config, region)
return config
Strict Type-Checked Configuration Loading¶
Type Checking:
# typed_config.py
from pydantic import BaseModel, Field
from typing import Literal
class DatabaseConfig(BaseModel):
host: str = Field(..., description="Database host")
port: int = Field(5432, ge=1, le=65535, description="Database port")
database: str = Field(..., description="Database name")
pool_size: int = Field(10, ge=1, le=100, description="Connection pool size")
class AppConfig(BaseModel):
name: str
environment: Literal["dev", "staging", "prod"]
database: DatabaseConfig
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
# Load with validation
config = AppConfig(**load_raw_config())
Single-Source-of-Truth Values¶
Pattern:
# Single source of truth
# config/base/app.yaml
app:
name: myapp
version: 1.2.3
# Environments reference base
# config/environments/dev/app.yaml
app:
# Inherit from base
name: ${base.app.name}
version: ${base.app.version}
# Only override what's different
debug: true
"Configuration Contracts" with Versioned Validation Rules¶
Contract Definition:
# config-contract.yaml
apiVersion: config.example.com/v1
kind: ConfigContract
metadata:
name: app-config-contract
version: 1.0.0
spec:
schema:
type: object
properties:
database_url:
type: string
pattern: "^postgresql://"
log_level:
type: string
enum: ["DEBUG", "INFO", "WARNING", "ERROR"]
required: ["database_url", "log_level"]
validation_rules:
- name: "database_url_format"
rule: "must match postgresql:// pattern"
- name: "log_level_enum"
rule: "must be one of DEBUG, INFO, WARNING, ERROR"
Feature Flags Aligned with Environment Tiers¶
Feature Flag Strategy:
# feature-flags.yaml
feature_flags:
new_ui:
dev: true
staging: true
prod: false # Not ready for prod
experimental_api:
dev: true
staging: false # Too risky for staging
prod: false
performance_optimization:
dev: true
staging: true
prod: true # Safe for all environments
Temporal Configuration Behavior¶
Temporal Configs:
# temporal-config.yaml
temporal_configs:
- name: "maintenance_window"
schedule: "0 2 * * 0" # Sunday 2 AM
config:
maintenance_mode: true
- name: "high_traffic_period"
schedule: "0 9 * * 1-5" # Weekdays 9 AM
config:
replicas: 10
cache_ttl: 300
Docker Compose Overrides¶
Pattern:
# docker-compose.base.yaml
services:
app:
image: app:latest
environment:
- DATABASE_URL=${DATABASE_URL}
- LOG_LEVEL=INFO
# docker-compose.dev.yaml
services:
app:
environment:
- LOG_LEVEL=DEBUG
volumes:
- ./src:/app/src
# docker-compose.prod.yaml
services:
app:
environment:
- LOG_LEVEL=INFO
deploy:
replicas: 3
Usage:
# Dev
docker-compose -f docker-compose.base.yaml -f docker-compose.dev.yaml up
# Prod
docker-compose -f docker-compose.base.yaml -f docker-compose.prod.yaml up
Helm Charts / Kustomize Overlays¶
Helm Pattern:
# values-base.yaml
replicas: 1
image:
repository: app
tag: latest
resources:
requests:
cpu: 100m
memory: 256Mi
# values-dev.yaml
replicas: 1
image:
tag: dev
resources:
requests:
cpu: 100m
memory: 256Mi
# values-prod.yaml
replicas: 5
image:
tag: v1.2.3
resources:
requests:
cpu: 1000m
memory: 2Gi
Kustomize Pattern:
kustomize/
├── base/
│ ├── kustomization.yaml
│ └── deployment.yaml
├── dev/
│ ├── kustomization.yaml
│ └── patches/
│ └── replicas.yaml
└── prod/
├── kustomization.yaml
└── patches/
└── replicas.yaml
Postgres Config Files¶
Postgres Config:
# postgresql.base.conf
shared_buffers = 256MB
max_connections = 100
work_mem = 4MB
# postgresql.dev.conf (override)
shared_buffers = 128MB
max_connections = 50
# postgresql.prod.conf (override)
shared_buffers = 1GB
max_connections = 200
Prefect Flows¶
Prefect Config:
# prefect_config.py
from prefect import flow
from pydantic import BaseSettings
class PrefectConfig(BaseSettings):
environment: str
database_url: str
redis_url: str
class Config:
env_file = f".env.{environment}"
@flow
def etl_flow(config: PrefectConfig):
"""ETL flow with environment-aware config"""
# Use config
pass
Air-Gapped Configs¶
Air-Gapped Pattern:
# Package configs for air-gapped deployment
tar -czf config-bundle-$(date +%Y%m%d).tar.gz \
config/ \
schemas/ \
secrets-encrypted/ \
checksums.txt
# Verify on air-gapped cluster
sha256sum -c checksums.txt
Anti-Patterns and Real-World Pitfalls¶
Overloaded .env Files¶
Problem: Single .env file with everything.
Example:
# .env (overloaded)
DATABASE_URL=...
REDIS_URL=...
API_KEY=...
SECRET_KEY=...
LOG_LEVEL=...
DEBUG=...
# 100+ variables, no organization
Fix: Organize by domain and environment.
# .env.database
DATABASE_URL=...
DATABASE_POOL_SIZE=...
# .env.redis
REDIS_URL=...
REDIS_TTL=...
# .env.app
LOG_LEVEL=...
DEBUG=...
Copy-Pasted YAML¶
Problem: YAML files copied between environments.
Example:
# config/dev/app.yaml
app:
name: myapp
database_url: postgresql://dev-db/app
# config/prod/app.yaml (copy-pasted, forgot to change)
app:
name: myapp
database_url: postgresql://dev-db/app # Wrong! Still pointing to dev
Fix: Use inheritance and templating.
# config/base/app.yaml
app:
name: myapp
# config/environments/dev/app.yaml
app:
database_url: postgresql://dev-db/app
# config/environments/prod/app.yaml
app:
database_url: postgresql://prod-db/app
Environment-Specific Logic Hidden in Code¶
Problem: Environment checks in application code.
Example:
# Bad: Environment logic in code
if os.getenv("ENVIRONMENT") == "dev":
database_url = "postgresql://dev-db/app"
elif os.getenv("ENVIRONMENT") == "prod":
database_url = "postgresql://prod-db/app"
Fix: Externalize to configuration.
Divergent Secrets Management Systems¶
Problem: Different secret management in each environment.
Fix: Standardize on one system.
# Standardized secrets management
secrets_management:
system: "vault" # Same for all environments
backend: "vault-backend"
paths:
dev: "secret/data/dev"
prod: "secret/data/prod"
Stale or Undocumented Toggles¶
Problem: Feature flags never cleaned up.
Fix: Document and audit feature flags.
# feature-flags.yaml
feature_flags:
old_feature:
enabled: false
deprecated: true
removal_date: "2024-06-01"
owner: "team-a"
new_feature:
enabled: true
description: "New feature description"
owner: "team-b"
Per-Developer Configuration Mutations¶
Problem: Each developer has different local config.
Fix: Standardize local development.
# .devcontainer/devcontainer.json
{
"name": "Standard Dev Environment",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace"
}
Configuration with Implicit Default Changes¶
Problem: Defaults change without notice.
Example:
# Old version: default replicas = 1
# New version: default replicas = 3
# Deployments change without explicit config
Fix: Always specify values explicitly.
Manual Patching of RKE2 Nodes¶
Problem: Manual changes to nodes cause drift.
Fix: Use GitOps and automation.
# GitOps: All changes through Git
apiVersion: argoproj.io/v1alpha1
kind: Application
spec:
source:
repoURL: https://github.com/org/config-repo
path: k8s/
Drift Introduced by "Snowflake" Servers¶
Problem: Servers configured differently.
Fix: Use infrastructure as code.
# Infrastructure as code
resources:
- type: server
name: app-server-1
config: standard-app-server-config
- type: server
name: app-server-2
config: standard-app-server-config # Same config
Environment Promotion Pipelines¶
Dev → Test Promotion¶
Pipeline:
graph LR
Dev["Dev Environment"]
Validate["Validate Config"]
Test["Test Environment"]
Verify["Verify Deployment"]
Dev --> Validate
Validate --> Test
Test --> Verify
style Dev fill:#e1f5ff
style Validate fill:#fff4e1
style Test fill:#e8f5e9
style Verify fill:#f3e5f5 Implementation:
# .github/workflows/promote-dev-to-test.yml
name: Promote Dev to Test
on:
workflow_dispatch:
jobs:
promote:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate dev config
run: |
python scripts/validate_config.py config/environments/dev/
- name: Diff dev and test
run: |
python scripts/diff_configs.py dev test
- name: Promote to test
run: |
python scripts/promote_config.py dev test
- name: Deploy to test
run: |
kubectl apply -f config/environments/test/
Test → Staging Promotion¶
Pipeline:
# .github/workflows/promote-test-to-staging.yml
name: Promote Test to Staging
on:
workflow_dispatch:
inputs:
approval:
description: 'Approve promotion'
required: true
type: boolean
jobs:
promote:
if: github.event.inputs.approval == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate test config
run: |
python scripts/validate_config.py config/environments/test/
- name: Schema promotion check
run: |
python scripts/check_schema_promotion.py test staging
- name: Promote to staging
run: |
python scripts/promote_config.py test staging
Staging → Prod Promotion¶
Pipeline:
# .github/workflows/promote-staging-to-prod.yml
name: Promote Staging to Prod
on:
workflow_dispatch:
inputs:
approval:
description: 'Approve production promotion'
required: true
type: boolean
jobs:
promote:
if: github.event.inputs.approval == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Validate staging config
run: |
python scripts/validate_config.py config/environments/staging/
- name: Production readiness check
run: |
python scripts/check_prod_readiness.py staging
- name: Promote to prod
run: |
python scripts/promote_config.py staging prod
- name: Deploy to prod
run: |
kubectl apply -f config/environments/prod/
Automated Diffing¶
Diff Tool:
# scripts/diff_configs.py
import yaml
from deepdiff import DeepDiff
def diff_configs(env1: str, env2: str):
"""Diff configurations between environments"""
config1 = load_config(env1)
config2 = load_config(env2)
diff = DeepDiff(config1, config2, ignore_order=True)
if diff:
print(f"Differences between {env1} and {env2}:")
print(diff.pretty())
return False
else:
print(f"No differences between {env1} and {env2}")
return True
Schema Promotion Policies¶
Policy:
# schema-promotion-policy.yaml
schema_promotion:
rules:
- name: "backward_compatible_only"
rule: "Only backward-compatible schema changes allowed"
enforcement: "strict"
- name: "migration_required"
rule: "Breaking changes require migration plan"
enforcement: "strict"
- name: "version_bump"
rule: "Schema changes require version bump"
enforcement: "strict"
Config Gates and Approval Workflows¶
Gate Configuration:
# config-gates.yaml
gates:
- name: "config_validation"
type: "automated"
checks:
- "schema_validation"
- "type_validation"
- "constraint_validation"
- name: "security_review"
type: "manual"
approvers:
- "security-team"
- name: "architecture_review"
type: "manual"
approvers:
- "architecture-team"
Reproducible Environment Snapshots¶
Snapshot Creation:
# scripts/create_snapshot.py
def create_environment_snapshot(environment: str) -> dict:
"""Create reproducible environment snapshot"""
snapshot = {
'timestamp': datetime.now().isoformat(),
'environment': environment,
'config': load_config(environment),
'schemas': load_schemas(environment),
'secrets_hash': hash_secrets(environment),
'images': get_image_versions(environment),
'checksum': None
}
# Calculate checksum
snapshot['checksum'] = calculate_checksum(snapshot)
return snapshot
Config Freeze Periods¶
Freeze Policy:
# config-freeze-policy.yaml
freeze_periods:
- name: "year_end_freeze"
start: "2024-12-15"
end: "2025-01-05"
environments: ["prod"]
exceptions:
- "critical_security_patches"
- "emergency_fixes"
Automated Config Migration Scripts¶
Migration Script:
# scripts/migrate_config.py
class ConfigMigrator:
def migrate(self, source_env: str, target_env: str):
"""Migrate configuration between environments"""
source_config = load_config(source_env)
target_config = load_config(target_env)
# Apply migrations
migrations = load_migrations()
for migration in migrations:
if migration.applies_to(source_config, target_config):
target_config = migration.apply(target_config)
# Save migrated config
save_config(target_env, target_config)
Config Rollback Procedures¶
Rollback Script:
#!/bin/bash
# scripts/rollback_config.sh
ENVIRONMENT=$1
VERSION=$2
echo "Rolling back ${ENVIRONMENT} to version ${VERSION}"
# Restore config from version
git checkout ${VERSION} -- config/environments/${ENVIRONMENT}/
# Validate restored config
python scripts/validate_config.py config/environments/${ENVIRONMENT}/
# Apply restored config
kubectl apply -f config/environments/${ENVIRONMENT}/
echo "Rollback complete"
Automated Drift Detection & Enforcement¶
GitOps Enforcement Patterns¶
ArgoCD Configuration:
# argocd-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: app-config
spec:
source:
repoURL: https://github.com/org/config-repo
path: k8s/
targetRevision: main
destination:
server: https://kubernetes.default.svc
namespace: default
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Temporal Auditing of Runtime Configs¶
Audit Script:
# audit/runtime_config_audit.py
class RuntimeConfigAuditor:
def audit_runtime_configs(self):
"""Audit runtime configurations"""
# Get desired config from Git
desired_config = load_config_from_git()
# Get actual config from cluster
actual_config = get_config_from_cluster()
# Compare
drift = compare_configs(desired_config, actual_config)
if drift:
# Alert on drift
alert_on_drift(drift)
# Optionally auto-remediate
if self.auto_remediate_enabled():
self.remediate_drift(drift)
return drift
Kubernetes Cluster State Diffing¶
State Diff Tool:
# drift/k8s_state_diff.py
class KubernetesStateDiffer:
def diff_cluster_state(self, cluster: str):
"""Diff Kubernetes cluster state"""
# Get desired state from Git
desired_state = load_kubernetes_manifests()
# Get actual state from cluster
actual_state = get_cluster_state(cluster)
# Diff
diff = kubectl_diff(desired_state, actual_state)
return diff
Postgres Config/State Hash Monitors¶
Postgres Monitor:
# monitoring/postgres_config_monitor.py
class PostgresConfigMonitor:
def monitor_postgres_config(self, instance: str):
"""Monitor Postgres configuration"""
# Get config hash
config_hash = get_postgres_config_hash(instance)
# Compare with expected
expected_hash = get_expected_config_hash(instance)
if config_hash != expected_hash:
alert_on_config_drift(instance, config_hash, expected_hash)
Redis Config Fingerprinting¶
Redis Fingerprint:
# monitoring/redis_fingerprint.py
class RedisConfigFingerprinter:
def fingerprint_redis_config(self, instance: str) -> str:
"""Generate Redis config fingerprint"""
config = get_redis_config(instance)
# Create fingerprint
fingerprint = hashlib.sha256(
json.dumps(config, sort_keys=True).encode()
).hexdigest()
return fingerprint
FDW Target Drift Detection¶
FDW Drift Detection:
-- Check FDW target consistency
SELECT
srvname,
srvoptions,
md5(srvoptions::text) AS config_hash
FROM pg_foreign_server
WHERE srvname LIKE 's3_%';
-- Compare hashes across environments
-- Alert if hashes differ
Docker Image SBOM Diffing¶
SBOM Diff Tool:
# drift/sbom_diff.py
class SBOMDiffer:
def diff_sboms(self, image1: str, image2: str) -> dict:
"""Diff SBOMs between images"""
sbom1 = get_sbom(image1)
sbom2 = get_sbom(image2)
diff = {
'added_packages': find_added_packages(sbom1, sbom2),
'removed_packages': find_removed_packages(sbom1, sbom2),
'version_changes': find_version_changes(sbom1, sbom2)
}
return diff
Runbook Automation for Reconciliation¶
Reconciliation Runbook:
#!/bin/bash
# runbooks/reconcile_config_drift.sh
echo "=== Config Drift Reconciliation ==="
# 1. Detect drift
DRIFT=$(python scripts/detect_drift.py)
if [ -z "$DRIFT" ]; then
echo "No drift detected"
exit 0
fi
# 2. Analyze drift
echo "Analyzing drift..."
python scripts/analyze_drift.py "$DRIFT"
# 3. Reconcile
echo "Reconciling drift..."
python scripts/reconcile_drift.py "$DRIFT"
# 4. Verify
echo "Verifying reconciliation..."
python scripts/verify_reconciliation.py
echo "=== Reconciliation complete ==="
Alerting Patterns¶
Prometheus Alerts:
# alerts/config_drift.yaml
groups:
- name: config_drift
rules:
- alert: ConfigDriftDetected
expr: config_drift_score > 0.1
for: 5m
annotations:
summary: "Configuration drift detected"
description: "Config drift score: {{ $value }}"
- alert: PostgresConfigDrift
expr: postgres_config_hash != postgres_expected_config_hash
for: 1m
annotations:
summary: "Postgres configuration drift"
description: "Postgres config hash mismatch"
Configuration Metadata & Versioning¶
Config Lineage¶
Lineage Tracking:
# metadata/lineage.py
class ConfigLineageTracker:
def track_lineage(self, config: dict) -> dict:
"""Track configuration lineage"""
lineage = {
'config_id': generate_config_id(config),
'source': 'git',
'commit': get_git_commit(),
'author': get_git_author(),
'timestamp': datetime.now().isoformat(),
'parent_configs': get_parent_configs(config),
'derived_configs': get_derived_configs(config)
}
return lineage
Environment Tagging¶
Tagging Strategy:
# config-tags.yaml
tags:
environment: "prod"
cluster: "prod-us-east-1"
region: "us-east-1"
version: "1.2.3"
deployment_id: "deploy-20240115-120000"
Build-ID Embedding¶
Build ID Injection:
# build_id_injection.py
class BuildIDInjector:
def inject_build_id(self, config: dict) -> dict:
"""Inject build ID into configuration"""
build_id = os.getenv("BUILD_ID") or generate_build_id()
config['metadata'] = {
'build_id': build_id,
'build_timestamp': datetime.now().isoformat(),
'git_commit': get_git_commit(),
'git_branch': get_git_branch()
}
return config
Schema Versioning¶
Schema Version Strategy:
# schema-versioning.yaml
schemas:
postgres:
current_version: "2.0.0"
versions:
- version: "1.0.0"
migration: "migrations/001_initial.sql"
- version: "2.0.0"
migration: "migrations/002_add_users.sql"
api:
current_version: "1.5.0"
versions:
- version: "1.0.0"
openapi: "api/v1/openapi.yaml"
- version: "1.5.0"
openapi: "api/v1.5/openapi.yaml"
Config Fingerprinting¶
Fingerprint Generation:
# fingerprinting.py
class ConfigFingerprinter:
def generate_fingerprint(self, config: dict) -> str:
"""Generate configuration fingerprint"""
# Normalize config (remove metadata, sort keys)
normalized = self.normalize_config(config)
# Generate hash
fingerprint = hashlib.sha256(
json.dumps(normalized, sort_keys=True).encode()
).hexdigest()
return fingerprint
Artifact Provenance¶
Provenance Tracking:
# provenance.yaml
provenance:
config_id: "config-abc123"
source:
repo: "https://github.com/org/config-repo"
commit: "abc123def456"
path: "config/environments/prod/"
build:
build_id: "build-xyz789"
build_timestamp: "2024-01-15T12:00:00Z"
deployment:
deployed_by: "ci-system"
deployed_at: "2024-01-15T12:05:00Z"
deployed_to: "prod-cluster"
In-Cluster Validation Hooks¶
Validation Webhook:
# validation/webhook.py
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/validate", methods=["POST"])
def validate_config():
"""Kubernetes validation webhook"""
admission_review = request.json
# Extract config
config = admission_review["request"]["object"]
# Validate
try:
validate_configuration(config)
allowed = True
message = "Configuration valid"
except ValidationError as e:
allowed = False
message = str(e)
# Return response
return jsonify({
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": admission_review["request"]["uid"],
"allowed": allowed,
"status": {"message": message}
}
})
Environment-Aware Dashboards¶
Dashboard Configuration:
{
"dashboard": {
"title": "Configuration Health by Environment",
"panels": [
{
"title": "Config Drift by Environment",
"targets": [
{
"expr": "config_drift_score{environment=\"dev\"}",
"legendFormat": "Dev"
},
{
"expr": "config_drift_score{environment=\"prod\"}",
"legendFormat": "Prod"
}
]
},
{
"title": "Config Version Distribution",
"targets": [
{
"expr": "count by (version) (config_versions)",
"legendFormat": "{{version}}"
}
]
}
]
}
}
Secrets Lifecycle Governance¶
Sealed Secrets¶
Sealed Secrets Pattern:
# sealed-secret.yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: database-credentials
spec:
encryptedData:
password: AgBy3i4OJSWK+PiTySYZZA9rO43cGDEQAx...
Age/SOPS Tooling¶
SOPS Configuration:
# .sops.yaml
creation_rules:
- path_regex: secrets/dev/.*
kms: "arn:aws:kms:us-east-1:123456789012:key/abc123"
- path_regex: secrets/prod/.*
kms: "arn:aws:kms:us-east-1:123456789012:key/xyz789"
Usage:
HashiCorp Vault Patterns¶
Vault Integration:
# vault-config.yaml
vault:
address: "https://vault.example.com"
auth:
method: "kubernetes"
role: "app-role"
paths:
dev: "secret/data/dev"
prod: "secret/data/prod"
External Secrets Operator:
# external-secret.yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
spec:
secretStoreRef:
name: vault-backend
kind: SecretStore
target:
name: database-credentials
data:
- secretKey: password
remoteRef:
key: secret/data/prod/database
property: password
Rotation Schedules¶
Rotation Policy:
# rotation-policy.yaml
rotation:
schedules:
database_passwords:
frequency: "90 days"
environments:
- prod
- staging
api_keys:
frequency: "180 days"
environments:
- prod
tls_certificates:
frequency: "365 days"
environments:
- prod
KMS Integration¶
KMS Configuration:
# kms-config.yaml
kms:
provider: "aws"
region: "us-east-1"
key_id: "arn:aws:kms:us-east-1:123456789012:key/abc123"
encryption_context:
environment: "prod"
service: "database"
Ephemeral Secrets for Ephemeral Environments¶
Ephemeral Secret Pattern:
# ephemeral_secrets.py
class EphemeralSecretManager:
def create_ephemeral_secret(self, environment: str, ttl: int = 3600):
"""Create ephemeral secret for temporary environment"""
secret = generate_secret()
# Store with TTL
store_secret_with_ttl(environment, secret, ttl)
return secret
Redaction Rules¶
Redaction Configuration:
# redaction-rules.yaml
redaction:
rules:
- pattern: "password.*"
action: "redact"
replacement: "***REDACTED***"
- pattern: "api_key.*"
action: "redact"
replacement: "***REDACTED***"
- pattern: "secret.*"
action: "redact"
replacement: "***REDACTED***"
Integration with Postgres pgaudit¶
PgAudit Integration:
-- Configure pgaudit for secret access
ALTER SYSTEM SET pgaudit.log = 'all';
ALTER SYSTEM SET pgaudit.log_catalog = off;
ALTER SYSTEM SET pgaudit.log_parameter = on;
-- Audit secret table access
CREATE TABLE secrets (
id SERIAL PRIMARY KEY,
key VARCHAR(255),
value TEXT,
environment VARCHAR(50)
);
-- Enable auditing
ALTER TABLE secrets ENABLE ROW LEVEL SECURITY;
Tooling & Implementation Examples¶
Helm Hierarchical Values¶
Values Structure:
# values.yaml (base)
replicas: 1
image:
repository: app
tag: latest
# values-dev.yaml
replicas: 1
image:
tag: dev
# values-prod.yaml
replicas: 5
image:
tag: v1.2.3
Usage:
Kustomize Overlays¶
Overlay Structure:
kustomize/
├── base/
│ ├── kustomization.yaml
│ └── deployment.yaml
├── dev/
│ ├── kustomization.yaml
│ └── patches/
│ └── replicas.yaml
└── prod/
├── kustomization.yaml
└── patches/
└── replicas.yaml
Base Kustomization:
# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
Dev Overlay:
# dev/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../base
patches:
- path: patches/replicas.yaml
Python Pydantic Settings¶
Pydantic Config:
# config.py
from pydantic import BaseSettings, Field
from typing import Literal
class DatabaseSettings(BaseSettings):
host: str = Field(..., env="DATABASE_HOST")
port: int = Field(5432, env="DATABASE_PORT")
database: str = Field(..., env="DATABASE_NAME")
pool_size: int = Field(10, ge=1, le=100)
class AppSettings(BaseSettings):
environment: Literal["dev", "staging", "prod"]
database: DatabaseSettings
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
class Config:
env_file = f".env.{environment}"
env_file_encoding = "utf-8"
# Load config
settings = AppSettings(environment="prod")
Go fx/Config Loader Patterns¶
Go Config:
// config.go
package config
import (
"github.com/spf13/viper"
)
type Config struct {
Environment string `mapstructure:"environment"`
Database DatabaseConfig `mapstructure:"database"`
LogLevel string `mapstructure:"log_level"`
}
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Database string `mapstructure:"database"`
}
func LoadConfig(env string) (*Config, error) {
viper.SetConfigName("config")
viper.SetConfigType("yaml")
viper.AddConfigPath(fmt.Sprintf("config/environments/%s/", env))
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
var config Config
if err := viper.Unmarshal(&config); err != nil {
return nil, err
}
return &config, nil
}
Rust figment/Config Crates¶
Rust Config:
// config.rs
use figment::{Figment, providers::{Format, Yaml, Env}};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize)]
struct Config {
environment: String,
database: DatabaseConfig,
log_level: String,
}
#[derive(Debug, Deserialize, Serialize)]
struct DatabaseConfig {
host: String,
port: u16,
database: String,
}
impl Config {
fn load(env: &str) -> Result<Self, figment::Error> {
Figment::new()
.merge(Yaml::file(format!("config/environments/{}/config.yaml", env)))
.merge(Env::prefixed("APP_"))
.extract()
}
}
Prefect Block Configurations¶
Prefect Blocks:
# prefect_blocks.py
from prefect.blocks.system import Secret
from prefect import flow
@flow
def etl_flow():
"""ETL flow with Prefect blocks"""
# Load secret block
db_password = Secret.load("database-password")
# Use in flow
database_url = f"postgresql://user:{db_password.get()}@host/db"
# ...
Postgres Config Templating via Jinja¶
Jinja Template:
# postgresql.conf.j2
# Auto-generated from template
shared_buffers = {{ shared_buffers }}
max_connections = {{ max_connections }}
work_mem = {{ work_mem }}
# Environment-specific
{% if environment == "prod" %}
shared_buffers = 1GB
max_connections = 200
{% else %}
shared_buffers = 256MB
max_connections = 100
{% endif %}
Template Rendering:
# render_postgres_config.py
from jinja2 import Template
def render_postgres_config(environment: str) -> str:
"""Render Postgres config from Jinja template"""
template = Template(open("postgresql.conf.j2").read())
context = {
'environment': environment,
'shared_buffers': get_shared_buffers(environment),
'max_connections': get_max_connections(environment),
'work_mem': get_work_mem(environment)
}
return template.render(**context)
GitHub Actions + Mike Versioned Docs¶
GitHub Actions:
# .github/workflows/deploy-docs.yml
name: Deploy Docs
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install mkdocs-material mike
- name: Deploy docs
run: |
mike deploy latest
mike set-default latest
Air-Gapped Config Packaging¶
Packaging Script:
#!/bin/bash
# scripts/package_airgap_config.sh
PACKAGE_NAME="config-bundle-$(date +%Y%m%d).tar.gz"
# Package configs
tar -czf "$PACKAGE_NAME" \
config/ \
schemas/ \
secrets-encrypted/ \
checksums.txt
# Sign package
gpg --sign "$PACKAGE_NAME"
# Create manifest
cat > manifest.json <<EOF
{
"package_name": "$PACKAGE_NAME",
"created_at": "$(date -Iseconds)",
"checksum": "$(sha256sum $PACKAGE_NAME | cut -d' ' -f1)",
"environments": ["dev", "staging", "prod"]
}
EOF
echo "Package created: $PACKAGE_NAME"
Agentic LLM Integration Hooks¶
Generate Safe Environment Configurations¶
LLM Config Generator:
# llm/config_generator.py
class LLMConfigGenerator:
def generate_config(self, requirements: dict) -> dict:
"""Generate safe environment configuration using LLM"""
prompt = f"""
Generate safe environment configuration for:
{json.dumps(requirements, indent=2)}
Requirements:
1. Follow security best practices
2. Use appropriate resource limits
3. Include proper validation
4. Document all settings
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a configuration generation expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Validate Configs for Inconsistencies¶
LLM Config Validator:
# llm/config_validator.py
class LLMConfigValidator:
def validate_config(self, config: dict) -> List[dict]:
"""Validate config for inconsistencies using LLM"""
prompt = f"""
Validate this configuration for inconsistencies:
{json.dumps(config, indent=2)}
Check for:
1. Inconsistent values
2. Security issues
3. Performance problems
4. Missing required fields
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a configuration validation expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Surface Hidden Drift¶
LLM Drift Detector:
# llm/drift_detector.py
class LLMDriftDetector:
def detect_hidden_drift(self, configs: dict) -> List[dict]:
"""Detect hidden configuration drift using LLM"""
prompt = f"""
Analyze these configurations for hidden drift:
{json.dumps(configs, indent=2)}
Identify:
1. Subtle differences
2. Implicit dependencies
3. Configuration patterns
4. Potential drift sources
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a drift detection expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Propose Environment Diffs¶
LLM Diff Proposer:
# llm/diff_proposer.py
class LLMDiffProposer:
def propose_diff(self, env1: str, env2: str) -> dict:
"""Propose environment diff using LLM"""
prompt = f"""
Propose configuration diff between {env1} and {env2}:
Environment 1 ({env1}):
{json.dumps(load_config(env1), indent=2)}
Environment 2 ({env2}):
{json.dumps(load_config(env2), indent=2)}
Provide:
1. Diff summary
2. Rationale for differences
3. Recommendations
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a configuration diff expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Generate Config Promotion Plans¶
LLM Promotion Planner:
# llm/promotion_planner.py
class LLMPromotionPlanner:
def generate_promotion_plan(self, source_env: str, target_env: str) -> dict:
"""Generate config promotion plan using LLM"""
prompt = f"""
Generate promotion plan from {source_env} to {target_env}:
Source environment:
{json.dumps(load_config(source_env), indent=2)}
Target environment:
{json.dumps(load_config(target_env), indent=2)}
Provide:
1. Promotion steps
2. Validation checks
3. Rollback plan
4. Risk assessment
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a configuration promotion expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Detect Misunderstood Settings¶
LLM Setting Analyzer:
# llm/setting_analyzer.py
class LLMSettingAnalyzer:
def analyze_settings(self, config: dict, system_info: dict) -> List[dict]:
"""Detect misunderstood settings using LLM"""
prompt = f"""
Analyze these settings for potential issues:
Configuration:
{json.dumps(config, indent=2)}
System Info:
{json.dumps(system_info, indent=2)}
Identify:
1. Settings that don't match system capacity
2. Incompatible settings
3. Performance issues
4. Security concerns
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a configuration analysis expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Produce Diagrams of Config Layering¶
LLM Diagram Generator:
# llm/diagram_generator.py
class LLMDiagramGenerator:
def generate_layering_diagram(self, config_structure: dict) -> str:
"""Generate Mermaid diagram of config layering"""
prompt = f"""
Generate a Mermaid diagram showing configuration layering:
{json.dumps(config_structure, indent=2)}
Show:
1. Base layer
2. Environment layer
3. Secrets layer
4. Runtime layer
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a diagram generation expert."},
{"role": "user", "content": prompt}
]
)
return response.choices[0].message.content
Audit Kubernetes Manifests for Environment Divergence¶
LLM K8s Auditor:
# llm/k8s_auditor.py
class LLMKubernetesAuditor:
def audit_manifests(self, manifests: List[dict]) -> dict:
"""Audit Kubernetes manifests for environment divergence"""
prompt = f"""
Audit these Kubernetes manifests for environment divergence:
{json.dumps(manifests, indent=2)}
Identify:
1. Environment-specific differences
2. Inconsistencies
3. Security issues
4. Best practice violations
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a Kubernetes audit expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Auto-Patch and Auto-Refactor Configuration Structure¶
LLM Config Refactorer:
# llm/config_refactorer.py
class LLMConfigRefactorer:
def refactor_config(self, config: dict, issues: List[str]) -> dict:
"""Auto-refactor configuration structure using LLM"""
prompt = f"""
Refactor this configuration to address issues:
Configuration:
{json.dumps(config, indent=2)}
Issues:
{json.dumps(issues, indent=2)}
Provide refactored configuration that:
1. Addresses all issues
2. Maintains backward compatibility
3. Follows best practices
"""
response = self.llm_client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "You are a configuration refactoring expert."},
{"role": "user", "content": prompt}
]
)
return json.loads(response.choices[0].message.content)
Configuration Scoring Rubric¶
Scoring Criteria¶
Rubric:
# config-scoring-rubric.yaml
scoring_criteria:
versioning:
weight: 0.15
criteria:
- "All configs versioned": 10
- "Most configs versioned": 7
- "Some configs versioned": 4
- "No versioning": 0
validation:
weight: 0.20
criteria:
- "Comprehensive validation": 10
- "Basic validation": 7
- "Minimal validation": 4
- "No validation": 0
documentation:
weight: 0.15
criteria:
- "Complete documentation": 10
- "Good documentation": 7
- "Basic documentation": 4
- "No documentation": 0
drift_detection:
weight: 0.20
criteria:
- "Automated drift detection": 10
- "Manual drift detection": 7
- "Periodic checks": 4
- "No drift detection": 0
secrets_management:
weight: 0.15
criteria:
- "Centralized secrets management": 10
- "Standardized secrets": 7
- "Some secrets management": 4
- "No secrets management": 0
promotion_process:
weight: 0.15
criteria:
- "Automated promotion": 10
- "Standardized promotion": 7
- "Manual promotion": 4
- "No promotion process": 0
Scoring Calculation¶
Calculator:
# scoring/calculator.py
class ConfigScorer:
def calculate_score(self, config: dict) -> float:
"""Calculate configuration score"""
rubric = load_scoring_rubric()
scores = {}
for criterion, details in rubric['scoring_criteria'].items():
score = self.evaluate_criterion(config, criterion, details)
scores[criterion] = score * details['weight']
total_score = sum(scores.values())
return total_score
Template for Environment Configuration Governance¶
Governance Template¶
Template:
# governance-template.yaml
configuration_governance:
policy:
name: "Environment Configuration Governance Policy"
version: "1.0.0"
effective_date: "2024-01-01"
principles:
- "Configuration as a Product"
- "Declarative Configuration First"
- "Immutable Infrastructure"
- "Fail-Fast Validation"
- "Explicit Environment Boundaries"
- "Thin Environment Differences"
- "Version-Locked Everything"
standards:
versioning:
required: true
format: "semver"
validation:
required: true
tools: ["pydantic", "jsonschema"]
documentation:
required: true
format: "markdown"
drift_detection:
required: true
frequency: "daily"
processes:
promotion:
- "dev → test: Automated"
- "test → staging: Automated + Approval"
- "staging → prod: Automated + Approval + Review"
review:
frequency: "quarterly"
participants: ["architecture-team", "platform-team"]
metrics:
- "Config drift score"
- "Promotion success rate"
- "Validation failure rate"
- "Documentation coverage"
Checklists¶
Configuration Review Checklist¶
- All configs versioned
- Validation in place
- Documentation complete
- Drift detection enabled
- Secrets managed securely
- Promotion process defined
- Rollback procedures documented
- Monitoring configured
Environment Promotion Checklist¶
- Source environment validated
- Config diff reviewed
- Schema compatibility checked
- Secrets rotated if needed
- Promotion plan approved
- Rollback plan ready
- Monitoring enabled
- Post-promotion verification scheduled
See Also¶
- Configuration Management - Configuration governance
- Configuration Drift Detection - Drift prevention
- Repository Standardization - Repository governance
This guide provides a complete framework for cross-environment configuration strategy. Start with core principles, implement patterns, prevent drift, and continuously improve. The goal is consistent, reproducible configurations across all environments.