From harness-claude
Centralizes configuration, feature flags, and secrets management across microservices. Replaces per-service env vars with Zod validation, AWS Secrets Manager, LaunchDarkly for runtime changes without redeploys and audits.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Centralize configuration, feature flags, and secrets management across services.
Provides guides and best practices for managing app configuration: environment variables, hierarchies, secrets, feature flags, validation, and 12-factor principles. Use for multi-env setups.
Manages configs across dev/staging/prod with .env files, Kubernetes ConfigMaps/Secrets, AWS SSM. Audits values, encrypts secrets via sops, validates schemas, detects drift, enables promotion workflows.
Manages full lifecycle of secrets and environment variables: decides placement (constant, .env, CI secret, env var), scaffolds .env.example/.gitignore, add/update/rotate/remove/migrate/audit/provision across envs. Language-agnostic.
Share bugs, ideas, or general feedback.
Centralize configuration, feature flags, and secrets management across services.
Environment variables (baseline — always start here):
// config.ts — typed config validation at startup
import { z } from 'zod';
const ConfigSchema = z.object({
// Server
PORT: z.coerce.number().default(8080),
NODE_ENV: z.enum(['development', 'test', 'staging', 'production']).default('development'),
// Database
DATABASE_URL: z.string().url(),
DATABASE_POOL_MIN: z.coerce.number().default(2),
DATABASE_POOL_MAX: z.coerce.number().default(10),
// Redis
REDIS_URL: z.string().url(),
// External services
PAYMENT_SERVICE_URL: z.string().url(),
NOTIFICATION_SERVICE_URL: z.string().url(),
// Feature flags (simple boolean env vars)
FEATURE_NEW_CHECKOUT: z.coerce.boolean().default(false),
FEATURE_LOYALTY_PROGRAM: z.coerce.boolean().default(false),
});
// Validate on startup — fail fast if config is missing
const parseResult = ConfigSchema.safeParse(process.env);
if (!parseResult.success) {
console.error('Invalid configuration:', parseResult.error.format());
process.exit(1);
}
export const config = parseResult.data;
AWS Secrets Manager (for credentials):
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const secretsManager = new SecretsManagerClient({ region: process.env.AWS_REGION });
// Cache secrets to avoid per-request API calls
const secretCache = new Map<string, { value: unknown; expiresAt: number }>();
async function getSecret<T>(secretId: string, ttlMs = 300_000): Promise<T> {
const cached = secretCache.get(secretId);
if (cached && Date.now() < cached.expiresAt) return cached.value as T;
const command = new GetSecretValueCommand({ SecretId: secretId });
const response = await secretsManager.send(command);
const value = JSON.parse(response.SecretString!);
secretCache.set(secretId, { value, expiresAt: Date.now() + ttlMs });
return value as T;
}
// At startup: load secrets into config
const dbSecret = await getSecret<{ username: string; password: string }>(
`${process.env.NODE_ENV}/order-service/database`
);
const databaseUrl = `postgresql://${dbSecret.username}:${dbSecret.password}@${process.env.DB_HOST}/orders`;
Feature flags with LaunchDarkly (runtime flags):
import { LDClient, init as ldInit } from '@launchdarkly/node-server-sdk';
let ldClient: LDClient;
async function initFeatureFlags(): Promise<void> {
ldClient = ldInit(process.env.LAUNCHDARKLY_SDK_KEY!);
await ldClient.waitForInitialization({ timeout: 10 });
console.log('Feature flags initialized');
}
async function isFeatureEnabled(
flagKey: string,
userId: string,
defaultValue = false
): Promise<boolean> {
return ldClient.variation(flagKey, { key: userId }, defaultValue);
}
// Usage in route handler
app.post('/orders', async (req, res) => {
const useNewCheckout = await isFeatureEnabled('new-checkout-flow', req.user.id);
if (useNewCheckout) {
return newCheckoutFlow(req, res);
}
return legacyCheckoutFlow(req, res);
});
Kubernetes ConfigMap + Secret (for containerized services):
# configmap.yaml — non-sensitive config
apiVersion: v1
kind: ConfigMap
metadata:
name: order-service-config
namespace: production
data:
NODE_ENV: 'production'
DATABASE_POOL_MIN: '2'
DATABASE_POOL_MAX: '10'
PAYMENT_SERVICE_URL: 'http://payment-service:8080'
---
# secret.yaml — sensitive config (base64 encoded values)
apiVersion: v1
kind: Secret
metadata:
name: order-service-secrets
namespace: production
type: Opaque
stringData:
DATABASE_URL: 'postgresql://user:pass@postgres:5432/orders'
STRIPE_SECRET_KEY: 'sk_example_...'
---
# deployment.yaml — inject config into container
spec:
containers:
- name: order-service
envFrom:
- configMapRef:
name: order-service-config
- secretRef:
name: order-service-secrets
Consul KV for dynamic config:
import Consul from 'consul';
const consul = new Consul({ host: process.env.CONSUL_HOST });
class DynamicConfig {
private cache = new Map<string, unknown>();
async get<T>(key: string, defaultValue: T): Promise<T> {
try {
const result = await consul.kv.get(`config/order-service/${key}`);
if (!result) return defaultValue;
const value = JSON.parse(result.Value);
this.cache.set(key, value);
return value as T;
} catch {
return (this.cache.get(key) as T) ?? defaultValue;
}
}
// Watch for changes — update at runtime without restart
watch(key: string, callback: (value: unknown) => void): void {
const watcher = consul.watch({
method: consul.kv.get,
options: { key: `config/order-service/${key}` },
});
watcher.on('change', (data) => {
if (data) {
const value = JSON.parse(data.Value);
this.cache.set(key, value);
callback(value);
}
});
}
}
Configuration hierarchy (start simple, grow as needed):
Secrets rotation: Use AWS Secrets Manager or Vault with auto-rotation. Services should reload credentials on 401/403 responses (the secret may have rotated). Cache secrets with short TTLs (5-10 minutes).
Anti-patterns:
12-Factor App principle: Configuration belongs in the environment, not in the code. Everything that varies between environments (dev, staging, production) must be an environment variable or externalized config. Never hardcode URLs, ports, or credentials.
microservices.io/patterns/externalized-configuration.html