Managing Environment Lifecycle via CLI: Deploy, Clone, Stop, Start, Delete in One Tool
GuideSeptember 24, 202613 min read

Managing Environment Lifecycle via CLI: Deploy, Clone, Stop, Start, Delete in One Tool

Why the CLI Is the Right Tool for Lifecycle Management

A dashboard is good for exploring, for someone new to a system understanding what exists. It is a poor fit for anything that needs to be repeatable, scriptable, or run unattended in CI. Environment lifecycle management — create this, deploy that, tear this down once tests pass — is squarely the second category, which is why bns exists as a first-class interface, not an afterthought to the UI.

This guide is a practical tour of the full lifecycle, using real commands run against a live environment — not a paraphrase of --help output.

Creating an Environment

From a local bunnyshell.yaml:

Bash
1$ bns environments create \
2    --from-path bunnyshell.yaml \
3    --name "content-demo" \
4    --project 2grgN6gaM9 \
5    --k8s OY13OP4Gnr \
6    --deploy --no-wait
7
8Environment 5bDn8MrEgm successfully created... deploying...
9EventID        | xVb7xZMnJ8
10EnvironmentID  | 5bDn8MrEgm
11Status         | new
12Type           | env_queued

--deploy chains creation straight into a deploy pipeline; --no-wait returns immediately rather than blocking the terminal until the pipeline finishes (useful in scripts that want to do other work while the deploy runs in the background, then poll separately).

You can also create from a shared template (--from-template <TEMPLATE_ID>) or directly from a Git repository containing a bunnyshell.yaml (--from-git), rather than a local file — useful in CI where the definition lives in the repository being tested, not on the CI runner's local disk as a standalone file.

Checking Deploy Status

Bash
1$ bns pipeline show --id OG7nYbkwM3
2
3PipelineID     | OG7nYbkwM3
4Status         | success
5Stages | Name  | Duration | Jobs | JobsDone | Status
6       | Stage | 1m36s    | 8    | 8        | success

For scripting a wait-until-done loop rather than blocking on the CLI's own --wait behavior:

Bash
1until bns pipeline show --id "$PIPELINE_ID" -o json | jq -e '.status == "success" or .status == "failed"' > /dev/null; do
2  sleep 5
3done

Getting the Environment's Public URL

Bash
1$ bns environments endpoints --id 5bDn8MrEgm
2
3Name      | web
4Endpoints | https://web-jfq6ir.bunnyenv.com/

This is the value you'd feed to an E2E test runner's baseURL, a CI output variable, or a PR comment — real, immediately usable output rather than something you have to construct from the environment's namespace by convention.

Cloning

Bash
1$ bns environments clone --id 5bDn8MrEgm --name "content-demo-clone"
2
3Environment 5bDn8MrEgm successfully cloned:
4EnvironmentID   | LPDqgKKEzk
5Namespace       | otmwdf
6OperationStatus | draft

Note the clone starts in draft — cloning the definition is instant and free; deploying it (bns environments deploy --id LPDqgKKEzk) is the step that actually provisions compute. This two-step design means you can clone cheaply and decide later whether to actually deploy it.

Updating Configuration

To push a changed bunnyshell.yaml to an existing environment and redeploy in one step:

Bash
bns environments update-configuration --id <ENV_ID> \
  --from-path bunnyshell.yaml --deploy

Here's exactly this used to intentionally introduce a broken image tag, confirming the deploy pipeline correctly reports failure (see the debugging section below) — real output, not a hypothetical:

Bash
1$ bns environments update-configuration --id LPDqgKKEzk \
2    --from-path broken-def.yaml --k8s OY13OP4Gnr --deploy
3
4Environment LPDqgKKEzk successfully updated... deploying...
5Environment LPDqgKKEzk deploying failed
6Error: pipeline has encountered an error

Stopping and Starting

Bash
1$ bns environments stop --id 5bDn8MrEgm
2
3Environment 5bDn8MrEgm scheduled to stop with EventID j3G239e4N1
4Environment 5bDn8MrEgm successfully stopped
Bash
1$ bns environments start --id 5bDn8MrEgm
2
3Environment 5bDn8MrEgm scheduled to start with EventID WzG0MkArNg
4Environment 5bDn8MrEgm successfully started
5Name      | web
6Endpoints | https://web-jfq6ir.bunnyenv.com/

The endpoint is identical before and after the stop/start cycle — the environment's identity persists across a stop, only its running state and compute cost change. Full mechanics and when to prefer this over deletion are covered in Stop and Start Environments On-Demand.

Deleting

Bash
1$ bns environments delete --id 5bDn8MrEgm
2
3Environment 5bDn8MrEgm scheduled to delete with EventID V2GY16VBGR
4Environment 5bDn8MrEgm successfully deleted

Deletion is a real pipeline (its own ID, its own log), not an instant flag flip — because tearing down networking, storage, and compute cleanly takes real, sequenced steps, the same as provisioning them did.

Debugging a Failed Pipeline From the CLI

When a lifecycle operation fails, the CLI gives you the exact failure without needing to open a dashboard:

Bash
1$ bns pipelines jobs --id BRWpY2Op6Z --jobStatus failed
2
3JobID      | Name                     | Status | Duration
4gV4ggDJzy4 | Check environment status | failed | 20s
Bash
1$ bns pipelines logs --id BRWpY2Op6Z --job-status failed --step-status failed
2
3✘  Step: check component status #1  failed  (exit: 1)
409:08:51.687  cluster situation preview:
5              ➡️ Component "web" -> 🔴 err
6                Pod "env-otmwdf/web-5d9498ddf4-m2rfl" -> pod_err_container
7                  Container "web" -> container_error_pull_image
809:08:57.593  Events:
9                > [Warning] Failed to pull image "nginxdemos/hello:this-tag-does-not-exist-404": ...not found

This is real output from a deliberately broken deploy (a nonexistent image tag) — the exact failure reason (image pull error, on which pod, with which underlying Kubernetes event) is visible from a single command, without needing cluster-admin kubectl access to inspect pod events directly.

Scripting the Full Lifecycle

Putting the pieces together — a script suitable for a CI job that needs an ephemeral environment for the duration of a test run, then cleans up regardless of outcome:

Bash
1#!/bin/bash
2set -euo pipefail
3
4ENV_ID=$(bns environments create \
5  --from-path bunnyshell.yaml --name "ci-test-$(date +%s)" \
6  --project "$PROJECT_ID" --k8s "$CLUSTER_ID" --deploy \
7  -o json | jq -r '.id')
8
9trap 'bns environments delete --id "$ENV_ID"' EXIT
10
11URL=$(bns environments endpoints --id "$ENV_ID" -o json | jq -r '.[0].endpoints[0]')
12npx playwright test --config=playwright.config.ts
13# ENV_ID is deleted automatically by the trap, whether the test run passed or failed

The trap ... EXIT is the important detail — it guarantees cleanup runs whether the test suite passes, fails, or the script itself errors out partway, which is exactly the reliability property that ad hoc "remember to clean up" scripts tend to lack (see Auto-Destroy After Merge for the same reliability argument applied to PR-triggered environments specifically).

Output Formats for Scripting

Every read command supports -o json for scripts that need to parse specific fields rather than pattern-match human-readable tables:

Bash
bns environments list -o json | jq -r '.[] | select(.type=="ephemeral") | .id'

Note (from the CLI's own documentation): deploy/stop/start do not support --output json — these are action commands whose primary output is a real-time status stream, not structured data, so scripts orchestrating them should pair the action command (stylish output, checked for success/failure text or exit code) with a separate show call in JSON for anything needing structured post-action state.

CI/CD Integration Patterns

The scripted lifecycle shown above is written as a standalone bash script, but the same commands drop directly into any CI system's job definition. A GitHub Actions equivalent:

YAML
1# .github/workflows/e2e-with-ephemeral-env.yml
2jobs:
3  e2e:
4    runs-on: ubuntu-latest
5    steps:
6      - uses: actions/checkout@v4
7      - name: Install bns CLI
8        run: brew install bunnyshell/tap/bunnyshell-cli
9      - name: Create and deploy environment
10        id: env
11        run: |
12          ENV_ID=$(bns environments create --from-path bunnyshell.yaml \
13            --name "ci-${{ github.run_id }}" --project ${{ secrets.PROJECT_ID }} \
14            --k8s ${{ secrets.CLUSTER_ID }} --deploy -o json | jq -r '.id')
15          echo "env_id=$ENV_ID" >> "$GITHUB_OUTPUT"
16        env:
17          BUNNYSHELL_TOKEN: ${{ secrets.BUNNYSHELL_TOKEN }}
18      - name: Run E2E suite
19        run: npx playwright test
20        env:
21          PREVIEW_URL: ${{ steps.env.outputs.endpoint }}
22      - name: Cleanup
23        if: always()
24        run: bns environments delete --id ${{ steps.env.outputs.env_id }}
25        env:
26          BUNNYSHELL_TOKEN: ${{ secrets.BUNNYSHELL_TOKEN }}

The if: always() on the cleanup step is the CI-native equivalent of the bash trap ... EXIT shown earlier — it guarantees the environment is deleted whether the E2E job passed or failed, which matters exactly as much in a CI job as it does in a standalone script, for exactly the same reason (see Auto-Destroy After Merge).

CLI Version Compatibility Notes

Worth flagging explicitly, since it's a real gotcha teams run into: bns CLI behavior has changed meaningfully between versions. Commands like exec and logs (covered in SSH Into a Running Environment) do not exist at all in CLI versions prior to roughly 0.26 — running an older CLI against documentation written for a newer one produces a confusing unknown command error that looks like a typo rather than a version mismatch. If a documented command fails with "unknown command" or "unknown flag" and the syntax looks correct, check your installed version first:

Bash
bns --version
brew upgrade bunnyshell/tap/bunnyshell-cli   # macOS

This guide's examples were all run against bns 0.26.1 — worth noting your own team's pinned CI version explicitly in your automation's documentation, so a future debugging session doesn't waste time assuming a command is broken when it's actually just an older CLI than what the runbook was written against.

Comparing bns to a Terraform-Style Declarative Workflow

Teams accustomed to Terraform's plan/apply model sometimes ask how bns compares — whether there's an equivalent "show me what would change before I apply it" step. The closest analog is bns environments update-configuration combined with bns environments definition — exporting the current live definition, diffing it locally against your intended change (a plain git diff against the tracked bunnyshell.yaml, or any diff tool), and only then applying the update. This is a lighter-weight workflow than Terraform's formal plan step, deliberately so — bunnyshell.yaml describes application-level environment composition (which components, how they connect, what images), not low-level cloud infrastructure primitives, so the blast radius of an unreviewed change is more contained than an unreviewed Terraform apply touching IAM policies or network topology. Teams provisioning the underlying Kubernetes cluster or cloud infrastructure Bunnyshell deploys onto typically still use Terraform for that layer — see Preview Environments with Terraform — with bunnyshell.yaml and the bns CLI operating one layer up, at the application-environment level.

A Note on Idempotency

Re-running bns environments create with an identical bunnyshell.yaml does not update an existing environment of the same name — it attempts to create a new one, which will either succeed as a genuinely separate environment or fail if your setup enforces unique names, depending on configuration. This is a meaningful difference from Terraform's idempotent apply, which reconciles state whether or not resources already exist. The practical implication for scripting: a script intending to "ensure this environment exists and is up to date" should explicitly check for an existing environment first (bns environments list filtered by name or a label) and branch between create and update-configuration accordingly, rather than assuming repeated create calls are safe to run unconditionally.

Building an Internal CLI Wrapper for Your Team

Teams that end up scripting the same handful of bns command sequences repeatedly often benefit from wrapping them in a small internal tool — not to replace bns, but to encode the specific conventions (project ID, cluster ID, naming pattern, standard labels) your team always uses, so individual engineers don't need to remember or retype them:

Bash
1#!/bin/bash
2# my-team-env.sh — thin wrapper encoding our team's conventions
3PROJECT_ID="2grgN6gaM9"
4CLUSTER_ID="OY13OP4Gnr"
5
6case "$1" in
7  new)
8    bns environments create --from-path bunnyshell.yaml \
9      --name "$2" --project "$PROJECT_ID" --k8s "$CLUSTER_ID" \
10      --deploy --label "team=$(whoami)"
11    ;;
12  cleanup)
13    bns environments delete --id "$2"
14    ;;
15esac

This kind of thin wrapper is a common and reasonable pattern precisely because bunnyshell.yaml and the CLI are designed to be scripted against — the goal is reducing how much project-specific context an individual engineer needs to hold in their head for routine operations, not building an alternative to the CLI itself.

Handling Partial Failures in a Multi-Step Script

A realistic lifecycle script rarely succeeds or fails as one atomic unit — creation might succeed while deploy fails, or deploy might succeed while a subsequent configuration update fails partway. Writing scripts that handle this gracefully, rather than assuming every step either fully succeeds or the whole script aborts cleanly, matters for anything running unattended in CI:

Bash
1ENV_ID=$(bns environments create --from-path bunnyshell.yaml \
2  --name "ci-run-$$" --project "$PROJECT_ID" --k8s "$CLUSTER_ID" -o json | jq -r '.id')
3
4if ! bns environments deploy --id "$ENV_ID"; then
5  echo "Deploy failed — checking failed jobs before cleanup"
6  PIPELINE_ID=$(bns pipeline list --environment "$ENV_ID" -o json | jq -r '.[0].id')
7  bns pipelines logs --id "$PIPELINE_ID" --job-status failed --step-status failed
8  bns environments delete --id "$ENV_ID"
9  exit 1
10fi

Capturing the failure detail (via pipelines logs --job-status failed) before deleting the failed environment is the detail worth not skipping — once deleted, that specific pipeline's detailed logs may become harder to correlate with the now-gone environment's context, so surfacing the failure reason as part of the cleanup path, not as a separate manual step someone has to remember, keeps a CI failure actionable rather than just "the environment failed, good luck figuring out why."

Checklist

  • Environment creation, deploy, and cleanup are scripted, not manual steps in a runbook
  • Cleanup uses a trap or equivalent guaranteed-execution mechanism, not a "last step" that gets skipped on failure
  • JSON output is used for anything a script needs to parse programmatically
  • Failed pipelines are debugged via bns pipelines jobs/logs before escalating to direct cluster access
✦✦

Manage the full environment lifecycle without leaving your terminal.

14-day full-feature trial. No credit card required. Pay-as-you-go from $0.007/min per environment.

Frequently Asked Questions

Why use the CLI instead of the Bunnyshell dashboard for environment operations?

The CLI is scriptable — it fits into CI pipelines, internal tooling, and automation in a way clicking through a dashboard does not. It also gives exact, reproducible command sequences that can be documented and shared, rather than a series of UI steps that are harder to communicate precisely.

Does the CLI support everything the dashboard supports?

The core lifecycle operations — create, deploy, clone, stop, start, delete, and configuration updates — are all available via CLI, along with read operations like listing environments, components, and pipelines. Some account-level settings (like initial Git provider OAuth connection) are UI-only since they involve an OAuth redirect flow the CLI cannot handle.

Can I script a full environment lifecycle from creation to deletion?

Yes, that is the primary use case for CLI-based lifecycle management — a script or CI job that creates an environment, waits for deploy, runs tests, then deletes it, all through a sequence of bns commands with no manual intervention.

Does the CLI wait for operations to finish by default?

Yes, commands like deploy, stop, and start wait for their pipeline to finish by default and report success or failure — use --no-wait to fire the operation and return immediately, checking status separately via bns pipelines show.

What output formats does the CLI support for scripting?

stylish (human-readable, default), json, yaml, and raw — json is generally the right choice for scripts that need to parse specific fields programmatically rather than pattern-match on human-readable table output.