BLOG/CONFIGURATION MANAGEMENT
THEME
CONFIGURATION MANAGEMENT

Render Kubernetes manifests per environment

Stop maintaining near-identical YAML for staging and prod. Keep one manifest template, feed it per-environment vars, and apply — without pulling in a whole templating platform.

May 9, 2026 OrchStep Team 6 minROLE: Platform EngineerSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/render-k8s-manifests
VIEW SOURCE

Every Kubernetes setup hits the same fork. You have a Deployment for staging and a Deployment for production that differ by exactly three things — replica count, namespace, ingress host — and yet they live as two full copies of YAML. Change the container spec and you have to remember to change it in both. Forget one and the environments drift.

You don't always need Helm or Kustomize for this. If the difference is a handful of values, a render step over one template covers it.

One template, per-environment values

The render function takes a template and a set of vars and produces text. Put the per-environment values in environments: and the same template serves both:

orchstep.yml
name: k8s-manifests
defaults:
  app: api
  image: ghcr.io/acme/api:1.5.0
  stage: staging
  replicas: "2"
  host: staging.example.com

# Per-environment values feed the same templates.
environments:
  staging:
    vars: { stage: staging, replicas: "2", host: staging.example.com }
  production:
    vars: { stage: production, replicas: "5", host: example.com }

tasks:
  # orchstep run apply --env production
  apply:
    desc: "Render the Deployment + Ingress for one environment and apply"
    steps:
      - name: deployment
        func: render
        args:
          template: |
            apiVersion: apps/v1
            kind: Deployment
            metadata:
              name: {{ vars.app }}
              namespace: {{ vars.stage }}
            spec:
              replicas: {{ vars.replicas }}
              template:
                spec:
                  containers:
                    - name: {{ vars.app }}
                      image: {{ vars.image }}
        outputs:
          manifest: "{{ result.output }}"

      - name: ingress
        func: render
        do: "host: {{ vars.host }}   # ingress for {{ vars.stage }}"
        outputs:
          rule: "{{ result.output }}"

      - name: apply
        func: shell
        do: echo "kubectl apply -n {{ vars.stage }} (Deployment {{ vars.app }} x{{ vars.replicas }} + Ingress {{ vars.host }})"

The container spec exists once. staging gets 2 replicas in the staging namespace; production gets 5 in production with the apex host. To render the manifest to disk for kubectl apply -f, add args.output_file to the render step — here we keep it in an output so the demo is side-effect-free.

Render for one environment

orchstep run apply --env production

The --env selection feeds replicas: 5 and host: example.com into the template, the manifest comes out the back, and the apply step ships it. Same task, same template, different environment.

Diff before you apply

The reason teams reach for templating in the first place is to avoid surprises in prod. A dry-run lets you see the resolved plan — which environment's values went in — before kubectl touches the cluster:

orchstep run apply --env production --dry-run
STAGING VALUES
namespace: staging
replicas: 2
host: staging.example.com
PRODUCTION VALUES
namespace: production
replicas: 5
host: example.com

Same template, two value sets. The thing that differs is config, and it's the only thing that differs.

What you gained

ConcernTwo manifestsOrchStep render
Container specduplicatedwritten once
Per-env differencesscattered in both filesenvironments: vars
Add an environmentcopy the whole manifestone environments: entry
Render + applytwo manual stepsone task
Previeweyeball the YAML--env <name> --dry-run

Where Helm still wins

If you need charts, dependencies, a release history, and helm rollback, use Helm — that's its job. This pattern is for the common middle ground: a few manifests that differ by a few values, where a full chart is more machinery than the problem deserves.

Where to go next

The project above is a runnable, echo-only demo. Swap the final echo for a real kubectl apply to make it live.

#KUBERNETES#RENDER#ENVIRONMENTS#MANIFESTS
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — CONFIGURATION MANAGEMENT