How to Use kubectl From Shell Scripts Step by Step
Script kubectl safely: explicit context and namespace, wait and rollout status, output formats for scripts, label selectors and exit codes in automation.

Scripts that call kubectl fail in two characteristic ways: they run against the wrong cluster because the active context changed, and they continue before resources are ready. Both have simple fixes. This post gives the habits, and the next, kubectl with jq and jsonpath, covers extracting data. The examples assume kubectl 1.31.
Be explicit about the target
CTX=prod-eu
NS=shop
k() { kubectl --context "$CTX" --namespace "$NS" "$@"; }
[[ $(kubectl config current-context) == "$CTX" ]] || echo "note: active context differs" >&2
k get pods -l app=webNAME READY STATUS RESTARTS AGE
web-6d9f7c8b5-4xk2p 1/1 Running 0 2d
web-6d9f7c8b5-9tq7m 1/1 Running 0 2d
A tiny wrapper function makes every call explicit, so the script never depends on kubectl config use-context executed some other time. Running kubectl delete in the wrong cluster is a classic outage, and the guard costs one line. Never rely on the namespace default either.
Wait for readiness
k apply -f deploy/
k rollout status deployment/web --timeout=180s
k wait --for=condition=Ready pod -l app=web --timeout=120s
k wait --for=delete pod/old-job --timeout=60sdeployment "web" successfully rolled out
pod/web-6d9f7c8b5-4xk2p condition met
pod/web-6d9f7c8b5-9tq7m condition met
rollout status exits non-zero when the timeout passes or the rollout fails, so set -e stops the script at the right point. On failure, roll back with k rollout undo deployment/web, the pattern in rolling back a failed deploy. kubectl wait works for any condition, including Jobs (--for=condition=complete).
Loops over resources
for pod in $(k get pods -l app=web -o name); do
echo "== $pod"
k logs "$pod" --tail=5
done
k get pods -o custom-columns=NAME:.metadata.name,NODE:.spec.nodeName --no-headers-o name prints pod/web-..., which every kubectl command accepts back, so loops need no parsing. Names contain no spaces, so unquoted expansion is acceptable here, though quoting is still the habit. Avoid parsing the default table output with awk, because column widths and headers change; use -o jsonpath, custom-columns or JSON.
Check before you apply
k apply -f deploy/ --dry-run=server
k diff -f deploy/ || true
k apply -f deploy/ --server-side--dry-run=server sends the request to the API server for validation and admission checks without storing it. kubectl diff exits 1 when there are differences, which is why it needs || true under set -e. Secrets should come from a secret store or sealed manifests, not from files in the repository; see secrets in deploy scripts.
Retries and transient failures
Many kubectl failures are momentary. The API server restarts during a control-plane upgrade, a webhook times out under load, or a conflict error appears because a controller updated the object a moment after you read it. A script that stops at the first one forces a human to rerun it. A small retry helper that repeats a command a few times with a growing delay turns most of these into a log line.
Retry only operations that are safe to repeat. kubectl apply is declarative and idempotent, so repeating it is harmless. A kubectl create that failed halfway might have created the object, and a retry then fails with "already exists", which should be treated as success only if the existing object is the one you meant. Anything that triggers a side effect outside the cluster, such as a job that sends email, needs thought before it goes inside a retry loop.
Cleaning up and temporary resources
Scripts that start debug pods or temporary jobs leave them behind when they exit early. A trap on EXIT that deletes the named resource, with --ignore-not-found, keeps the cluster tidy even when the script fails. Give each run a unique name, built from the process id or a timestamp, so two parallel runs do not collide and the cleanup deletes only what this run created.
For one-off commands, kubectl run with the remove-on-exit and never-restart options creates a pod, attaches, and deletes it afterwards. It is handy for a quick DNS lookup or a database client, but the pod still has to pull its image and be scheduled, so budget a few seconds and do not call it in a tight loop. Labels such as created-by=deploy-script make leftovers easy to find and remove later with a single selector.
Permissions, service accounts and auditing
A script that runs in CI should not use a human's kubeconfig. Give it a service account bound to a role that allows exactly the verbs and namespaces the script needs. The command kubectl auth can-i create deployments --namespace shop checks a permission from the script's own identity, and running it at the start produces a clear message instead of a half-finished deploy that failed on the last step.
The API server's audit log records who did what, and a dedicated identity per pipeline makes that record useful. Avoid sharing one cluster-admin credential between jobs, because when something is deleted the log then says only that the shared account did it. Short-lived tokens, from a cloud provider's workload identity or a projected service account token, limit the damage if a log leaks a credential. Keep the kubeconfig out of the repository and out of build logs, and rotate anything that ever appeared in either.
Output you can log and compare
Scripts that run unattended need output that a person can read afterwards. Print a short line before each stage naming the context, namespace and resource, so a CI log shows what was touched without opening the manifests. Redirect noisy command output to a file and attach it to the job as an artifact, keeping the main log to one line per step. When a command fails, print the failing resource's recent events with kubectl describe or kubectl get events --sort-by=.lastTimestamp, since the reason a rollout stalled is nearly always there.
Make runs comparable. Capture the image tags and replica counts before and after a deploy into a small text file, and diff the two. A deploy that reports success while the pods still run the previous image is a failure that no exit code reveals, and a before and after list exposes it at once. The same file is the starting point for a rollback, since it records what was running.
Avoid printing secrets while doing this. Use describe and get on workloads freely, but never dump Secret objects into logs, and pass --v debugging levels only locally, because high verbosity prints request headers that can include bearer tokens.
One last safeguard is a confirmation for destructive commands. A deploy script can run unattended, but a cleanup that deletes namespaces or scales workloads to zero should require an explicit --yes flag or an interactive prompt that names the cluster. Printing the context and the object count before the prompt turns the "wrong cluster" mistake into something a person notices.
Where this does not apply
For repeatable deployments across environments, use Helm, Kustomize or a GitOps controller rather than scripts of kubectl commands; Helm from the command line covers the first. Calling the API directly with curl is possible but rarely better, as in the Kubernetes API with curl. kubectl is also tied to its server version: stay within one minor version of the cluster. See the kubectl reference and the quick reference.


