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.
blog/k8s-dev-loopYou 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:
#!/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=1mname: 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=1mSame 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.
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-runYou 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
| Concern | dev.sh | OrchStep |
|---|---|---|
| Wait until Ready | easy to forget after apply | explicit rollout step |
| Transient API flakes | none | retry: { max_attempts: 3, backoff_rate: 1.5 } |
| Rollback on failure | || after the command | catch: block |
| Image tag | positional $1 | named 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
- Quick Start — your first workflow in two minutes
- Error Handling — retry, catch, finally, timeouts
- Variables & Outputs — the four-level scoping model
Have a dev.sh that rebuilds and redeploys? The shape above came straight from orchstep init — try it against your cluster.
curl -fsSL https://orchstep.dev/install.sh | sh