Blue-green deployment worked perfectly. Except the green environment had different config.

The setup:

  • Blue environment: running production
  • Green environment: idle, ready for deploy
  • Deploy to green, test, switch traffic

What went wrong:

A feature flag had been flipped in blue weeks earlier — directly, through the admin UI. Nobody touched green. When the next release deployed to green and traffic switched over, the flag silently reverted to its old value and users lost a feature they'd been relying on for weeks. No deploy failed, no alert fired; the environment was simply telling the truth about its own stale config.

Once we audited, we found more drift:

  • Different database connection pool sizes
  • Different log levels
  • Different timeout values
  • API keys rotated in blue only

The fix — two kinds of config, two homes:

The naive fix list — "put config in Git" and "use an external config store" — points in opposite directions until you separate what belongs where.

Versioned config lives in Git and ships with the release. Pool sizes, timeouts, log levels, resource limits: these are properties of a release, so they belong in the repo as rendered manifests applied identically to blue and green. A GitOps controller — Argo CD in our case, Flux works equally well — continuously diffs live state against Git, so an out-of-band edit to either environment shows up as drift within minutes instead of at cutover. This is the same discipline we treat as table stakes in CI/CD pipeline design.

Runtime toggles live in one shared external service. Feature flags must not exist per-environment at all. We moved them to a single flag service (an OpenFeature-compatible provider) that both colors read from, so a flag flip applies to whichever environment is serving traffic. Flag state has one home; blue and green are just consumers.

And a gate before every cutover. The pipeline diffs the rendered config of both environments and refuses to switch traffic on any delta:

# refuse to cut over if blue and green disagree
diff \
  <(kubectl get cm app-config -n blue -o jsonpath='{.data}') \
  <(kubectl get cm app-config -n green -o jsonpath='{.data}') \
  || { echo "config drift detected - aborting cutover"; exit 1; }

Secrets got the same treatment: rotated in one external store and synced into both environments by an operator, never rotated "in blue" by hand. The database side of blue-green has its own traps — see our blue-green database nightmare.

Lesson: Blue-green deployments require blue-green config management. An idle environment drifts by default; only automation notices.


← Back to Lessons Learned