12-factor config: one artifact, many environments, fail fast Intermediate
This cluster answers: where config lives, and how to load and check it so that one image runs correctly in every environment, and missing config kills the process at startup instead of at request time.
The problem
Section titled “The problem”The payment service deploys at 17:00. The pod starts, the health check returns 200, traffic flows in. At 17:20 every refund request fails with 401: PAYMENT_API_SECRET_KEY was misnamed in the Secret, the code reads process.env.PAYMENT_API_SECRET_KEY ?? '' and sends an empty key.
A config error does not kill the process. It leaves the process alive and wrong. The goal of this cluster: make every config error a startup error, before the first request, with a message that lists every bad key.
Reading order
Section titled “Reading order”Loading diagram…
In this cluster
Section titled “In this cluster”| Part | The question it answers |
|---|---|
| 12-factor and build once | What is config according to 12-factor? Why not build a separate image for production? |
| Parse and validate env | How do you write a typed config module? Which keys may have defaults? How to name keys and handle a local .env? |
| Kubernetes and runtime | Are ConfigMap/Secret safe? Does a config change need a restart? Rotation, feature flags, drift? |
| Common mistakes | Which lines of env-reading code cause bugs, and how to fix them? Plus a production checklist. |
Decision table: type of setting → where it lives
Section titled “Decision table: type of setting → where it lives”| Type of value | Example | Where it lives | Why |
|---|---|---|---|
| Business constant, identical in every environment | MAX_ITEMS_PER_ORDER = 50, order code regex |
Code (constant) | Changing it changes behavior; it needs review and tests like code |
| Varies by environment, not secret | DATABASE_POOL_MAX, PAYMENT_API_BASE_URL, LOG_LEVEL |
Env var (from a ConfigMap) | Exactly the 12-factor definition of config |
| Secret | DB password, API key, signing key | Secret manager → injected as env var or file | Access control, audit, rotation; see Secure coding |
| Toggled at runtime, per user or by % | new checkout for 5% of users, kill switch | Feature flag service | Changes without a deploy, with targeting and audit |
| Business data edited by operators | shipping fee per province, list of banks | Database (with an admin screen) | This is data, not config; it needs history and edit permissions |
Rule of thumb:
- Same in every environment → code.
- Changes per deploy → config.
- Changes per user or by time of day → flag or data.
When to apply, exceptions
Section titled “When to apply, exceptions”Apply to every service that runs in more than one environment: a typed config module, validation at startup, no defaults for secrets and production URLs.
Reasonable exceptions:
- One-off scripts, internal CLIs: reading
process.envdirectly with minimal checks is enough. - Large structured config (routing rules, tenant lists): a file mounted from a ConfigMap or the database, still validated on load. Env vars do not suit deeply nested structures.
- Highly sensitive secrets: read from a mounted file or call the secret manager directly instead of env vars. OWASP notes that env vars can be read by other processes or leak into logs and dumps.
Summary
Section titled “Summary”Self-check:
- Why does rebuilding a separate image for production violate “build once, deploy many”, even if the code is identical?
- What is the harm during a deploy if
loadConfigreports errors one key at a time? - You edit a ConfigMap but the pod still runs the old value: what are two possible causes?
- What must the app support so that webhook secret rotation has no window where signatures are rejected?
Next: 12-factor and build once.