BLOG/DEVELOPER DAILY
THEME
DEVELOPER DAILY

A fast local Kubernetes loop

Build, load, apply, wait for rollout, tail logs. The inner loop every platform engineer retypes a hundred times a day — turned into one OrchStep task that waits until the pods are actually Ready.

Jun 17, 2026 OrchStep Team 6 minROLE: Platform EngineerSCALE: Any
RUNNABLE DEMO
Full source for this post: blog/k8s-dev-loop
VIEW SOURCE

You change one line in a handler. Now you have to: rebuild the image, load it into your cluster, re-apply the manifests, wait for the rollout, and tail the logs to see if it crashed. Five commands, in order, every single time. You either retype them from memory or you keep a half-broken dev.sh around that nobody else can read.

The retyping is fine right up until the rollout. kubectl apply returns instantly and lies to you — the old pods are still serving while the new ones come up. So you apply, immediately tail logs, see the old code, and waste a minute wondering why your change vanished.

This post turns that inner loop into one OrchStep task that runs the five commands in order and blocks until the new pods are genuinely Ready before it shows you logs. One binary, no daemon, no platform.

The before

The shell version everyone has a slightly different copy of:

DEV.SH (BEFORE)
#!/usr/bin/env bash
set -euo pipefail

TAG="${1:-dev}"
docker build -t web:"$TAG" .
kind load docker-image web:"$TAG" --name kind-dev
kubectl apply -k deploy/

# apply returns instantly — this is the part people forget
kubectl rollout status deploy/web --timeout=90s \
  || { kubectl rollout undo deploy/web; exit 1; }

kubectl logs deploy/web --tail=20 --since=1m
ORCHSTEP.YML (AFTER)
name: k8s-dev-loop
defaults:
  app: web
  tag: dev
  cluster: kind-dev
tasks:
  loop:
    steps:
      - name: build
        func: shell
        do: docker build -t {{ vars.app }}:{{ vars.tag }} .
      - name: load
        func: shell
        do: kind load docker-image {{ vars.app }}:{{ vars.tag }} --name {{ vars.cluster }}
      - name: apply
        func: shell
        do: kubectl apply -k deploy/
      - name: rollout
        func: shell
        do: kubectl rollout status deploy/{{ vars.app }} --timeout=90s
        retry:
          max_attempts: 3
          interval: "2s"
          backoff_rate: 1.5
        catch:
          - name: undo
            func: shell
            do: kubectl rollout undo deploy/{{ vars.app }}
      - name: logs
        func: shell
        do: kubectl logs deploy/{{ vars.app }} --tail=20 --since=1m

Same docker, kind, and kubectl commands. But the rollback is a catch: block instead of a ||, the tag is a named variable instead of $1, and the rollout status step is the explicit "wait until Ready" gate the script always glossed over.

The full demo

The runnable project for this post is below — click through it. The rollout status command already blocks until the deployment converges; wrapping it in retry rides out the brief window where the API server reports the deployment as not-yet-observed right after an apply.

orchstep.yml
name: k8s-dev-loop
# Override any of these at the CLI with --var.
defaults:
  app: web
  tag: dev
  cluster: kind-dev

tasks:
  # `orchstep run loop --var tag=$(git rev-parse --short HEAD)`
  loop:
    steps:
      - name: build
        func: shell
        do: echo "docker build -t {{ vars.app }}:{{ vars.tag }} ."
      - name: load
        func: shell
        do: echo "kind load docker-image {{ vars.app }}:{{ vars.tag }} --name {{ vars.cluster }}"
      - name: apply
        func: shell
        do: echo "kubectl apply -k deploy/"
      # rollout status blocks until the new pods are Ready (or the timeout trips).
      - name: rollout
        func: shell
        do: echo "kubectl rollout status deploy/{{ vars.app }} --timeout=90s"
        retry:
          max_attempts: 3
          interval: "2s"
          backoff_rate: 1.5
        catch:
          - name: undo
            func: shell
            do: echo "kubectl rollout undo deploy/{{ vars.app }}"
      - name: logs
        func: shell
        do: echo "kubectl logs deploy/{{ vars.app }} --tail=20 --since=1m"

The demo steps only echo, so the project runs anywhere. Swap each echo "..." for the real command and it drives your cluster.

Run it

orchstep run loop --var tag=$(git rev-parse --short HEAD)

Tag the image with the current commit, rebuild, reload, re-apply, wait for Ready, then tail. One command for the loop you were running by hand.

See it before it touches the cluster

Before you trust a new manifest path, resolve the whole plan without executing anything:

orchstep run loop --dry-run

You get the exact commands, with the tag variable already substituted, in order — including the catch: rollback that fires if the rollout never converges. Full tour: Previewing with Dry Run.

What you actually gained

Concerndev.shOrchStep
Wait until Readyeasy to forget after applyexplicit rollout step
Transient API flakesnoneretry: { max_attempts: 3, backoff_rate: 1.5 }
Rollback on failure|| after the commandcatch: block
Image tagpositional $1named vars.tag + --var
"What will this run?"read the script--dry-run prints the plan

This doesn't replace kubectl or your cluster — it just makes the five-step loop a named thing you can run, preview, and hand to the next person without a Slack thread.

Where to go next

Have a dev.sh that rebuilds and redeploys? The shape above came straight from orchstep init — try it against your cluster.

#KUBERNETES#PLATFORM#TASK-RUNNER#PRODUCTIVITY
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — DEVELOPER DAILY