Skip to main content

Environments & promotion: build once, promote

You have one immutable, signed artifact (lesson 5.3). Now it has to travel: from a dev environment where you experiment, through a staging environment that mirrors production, to production where real users live. This journey is promotion, and doing it well rests on two principles you've been building toward: promote the same artifact (never rebuild) and separate config from code (the same artifact runs everywhere, configured per environment). Get this wrong and you end up with a tangle of environment-specific pipelines that defeat the entire point.

What an environment is

An environment is a complete, isolated running copy of your system. The classic ladder:

  • Dev — where developers integrate and experiment; broken often, low stakes.
  • Staging (a.k.a. pre-prod) — a faithful mirror of production, used for final verification. Its value comes entirely from being like prod.
  • Production (prod) — the real thing, serving real users. Stakes are highest.

Promotion is the act of advancing a change up this ladder, gaining confidence at each rung before it reaches users.

ONE immutableartifact\nmy-app@sha256:9b2f…devstagingproductionsame artifactsame artifact

Principle 1: promote the artifact, don't rebuild it

The diagram's key feature: the same artifact flows up the ladder. Promotion moves the exact image (by digest) you tested in dev into staging, then into prod. You do not kick off a fresh build for each environment.

Why this matters so much: if each environment rebuilds, each gets a subtly different artifact, and "it passed in staging" tells you nothing about prod — you shipped something staging never saw. Building once and promoting the same bytes is what makes staging a meaningful gate. (This is the build-vs-deploy distinction from 5.3, applied across environments.)

Principle 2: separate config from code

If the same artifact runs in dev, staging, and prod, then what differs between them — the database URL, the replica count, the log level, the feature toggles — must live outside the artifact, as configuration. This is the separation of config from code: the immutable image is identical everywhere; environment-specific settings are layered on at deploy time (recall ConfigMaps and Secrets from Chapter 4).

This principle has a powerful consequence for how you organize deployments. Because only the config differs, you don't need a separate pipeline per environment — you need separate config per environment, applied to the same artifact.

The anti-pattern: one pipeline per environment

The trap many teams fall into: a deploy-dev pipeline, a separate deploy-staging pipeline, and a separate deploy-prod pipeline — each with copy-pasted, slightly-diverging logic. They drift apart over time, prod grows special-case steps the others never tested, and "works in staging" stops predicting "works in prod." Environment-specific pipeline forks are an anti-pattern.

The fix is to express environments as config differences, not pipeline differences:

Shared baseconfig\n(the common90%)dev overlay\n(2replicas, debuglogs)staging overlay\n(3replicas, prod-like)prod overlay\n(10replicas, real DB)

You keep one shared base definition, then a thin overlay per environment that patches only what changes. One artifact, one pipeline, one base config — and small per-environment overlays. In a GitOps world (lesson 5.5) these overlays live as separate Git paths, so "promote to prod" is literally "update the prod path to point at the new digest."

Templating tools: Kustomize and Helm

Two tools dominate "same base, per-environment overrides" for Kubernetes:

  • Kustomize — A patch-and-overlay approach (built into kubectl). You write a base set of manifests, then per-environment overlays that patch specific fields (replica count, image digest, env vars). No templating language — it merges YAML. Great when environments are mostly the same with small deltas.
  • Helm — A templating + packaging approach. A Helm chart is a parameterized package of manifests with {{ }} placeholders; you supply a values file per environment (values-prod.yaml) to fill them in. More powerful and more popular for distributing third-party apps, at the cost of a templating language to learn.
base/overlays/prod/(patch)chart (templates)values-prod.yaml

Both achieve the same goal: one definition, parameterized per environment. Choose Kustomize for simple overlays, Helm when you need templating or are packaging an app for others. Many teams use both (Helm to install third-party charts, Kustomize to overlay their own apps).

:::tip Durable vs dated The durable ideas are promote the same artifact, separate config from code, and one base + thin overlays per environment instead of forked pipelines. The dated part is which tool does the templating — Kustomize vs Helm vs whatever comes next. If a tool lets you parameterize one definition per environment without duplicating the whole thing, it satisfies the principle. :::

Common pitfalls

  • Rebuilding per environment. The cardinal sin again: a fresh build per env means staging never tested what prod runs. Promote the same digest.
  • One pipeline per environment. Forked deploy pipelines drift; prod accumulates untested special cases. Use one pipeline + per-environment config overlays.
  • Baking config into the image. Hardcoding the prod database URL into the artifact means you need a different artifact per environment — breaking "build once." Keep config outside the image.
  • A staging that doesn't match prod. Staging's only value is fidelity. If it differs materially (different scale, different config shape), "passed in staging" stops meaning anything.
  • Overlays that quietly diverge. Even overlays can rot. Keep the shared base large and the per-env overlays as thin as possible so differences stay visible and intentional.

Why it matters

Promotion advances one immutable artifact up the ladder — dev → staging → prod — gaining confidence at each rung, and it rests on two principles: promote the same artifact (never rebuild, or staging tested nothing real) and separate config from code (the identical image runs everywhere; only environment-specific settings differ). That second principle kills the common anti-pattern of forked per-environment pipelines — instead you keep one base definition plus thin per-environment overlays (separate Git paths in GitOps), templated with Kustomize (patch-and-overlay) or Helm (templating + values files). Build once, promote the same bytes, vary only the config. With promotion structured this way, we're ready for the model that makes the deploy itself declarative and self-correcting: GitOps.

Next: GitOps: pull-based reconciliation with Argo CD & Flux →