Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Workspaces

A workspace is a per-environment bundle of config + state. The shape is modelled on kubectl contexts: you can have many of them, exactly one is “current” at a time, and a -w flag lets you address a specific one for a single command without flipping the pointer.

This chapter covers the on-disk layout, the everyday init / use / list flow, the full roksbnkctl workspaces command tree, the -w / --workspace override, and the “parking-lot” pattern the end-to-end test uses to delete the workspace it’s currently inside.

The on-disk layout

Every workspace lives under ~/.roksbnkctl/<name>/:

~/.roksbnkctl/
  config.yaml                          # global; current_workspace pointer
  known_hosts                          # SSH host keys (shared across workspaces)
  default/                             # workspace "default"
    config.yaml                        # this workspace's inputs
    cluster-outputs.json               # post-apply cluster identity (when present)
    state/                             # BNK trial state
      terraform.tfstate
      terraform.tfvars
      kubeconfig                       # admin kubeconfig (mode 0600)
      tf-source/                       # bundled HCL extracted to disk
      scratch/                         # docker bind-mounts, helm caches
    state-cluster/                     # cluster-phase state (separate tree)
      terraform.tfstate
      cluster-phase-override.tfvars
  prod/                                # workspace "prod"
    config.yaml
    state/
    ...

Three things are worth calling out:

  • ~/.roksbnkctl/config.yaml is global — non-secret user-wide preferences plus the current_workspace pointer. It is not a workspace config; the per-workspace files live one level deeper.
  • state/ and state-cluster/ are intentionally separate so roksbnkctl cluster up and roksbnkctl up don’t tangle their Terraform state. Most users won’t touch either directly.
  • cluster-outputs.json is the persisted identity of the workspace’s ROKS cluster — written by cluster up or cluster register, read by roksbnkctl up so BNK trials don’t have to re-state cluster identity in every tfvars.

Override the base directory with the ROKSBNKCTL_HOME env var. Test fixtures use this; everyday users shouldn’t need it.

terraform.applied.tfvars — what’s deployed right now

v1.4.0 adds a per-phase snapshot of the effective Terraform var-file inputs that produced the workspace’s current state. After every successful terraform applyroksbnkctl cluster up, roksbnkctl bnk up, or the legacy single-shape roksbnkctl uproksbnkctl writes a canonical-HCL summary of “what var-files said” to the phase’s state directory. Re-create / audit / handoff workflows that previously needed config.yaml (or memory) now have a file-on-disk record of the inputs.

Where it lives

Workspace shapePhasePath
ShapeSplit / ShapeClusterOnlyCluster phase~/.roksbnkctl/<workspace>/state-cluster/terraform.applied.tfvars
ShapeSplitTrial phase~/.roksbnkctl/<workspace>/state/terraform.applied.tfvars
ShapeLegacySingleboth phases (collapsed)~/.roksbnkctl/<workspace>/state/terraform.applied.tfvars

On ShapeLegacySingle, the file is a union of all sources (since the legacy shape doesn’t separate cluster and trial state) and the header comment records phase=legacy-single so the reader doesn’t mistake it for either a cluster-only or trial-only snapshot. See PRD 07 §“Design” for the format spec.

What it captures

A canonical HCL var-file: one assignment per line, variables sorted alphabetically within each source section. Each section is preceded by a comment line documenting which source contributed the values:

  • # === from config.yaml === — vars derived from the workspace’s config.yaml (written to terraform.tfvars on disk).
  • # === from terraform.tfvars.user === — the workspace-local user override file. If the file doesn’t exist, the section header is # === from terraform.tfvars.user (missing) === and the body is empty.
  • # === from cluster-phase override ===state-cluster/cluster-phase-override.tfvars (cluster-phase snapshots only).

Source-attribution comments matter because the same variable can appear in multiple sources; the “winner” — the value Terraform actually used — is the last section to mention it. The comments let the reader trace why a particular value ended up live.

Lifecycle

  • Written after every successful terraform apply. Plan flows don’t write the snapshot — the name terraform.applied.tfvars would mislead if a plan-time write existed.
  • Overwritten each apply. If you want history, copy the file aside before re-running up or wire restic / a git commit hook against ~/.roksbnkctl/<workspace>/.
  • Untouched by destroy. cluster down / bnk down leave the prior up’s snapshot in place; that’s what was last deployed. The file’s mtime + the absence of Terraform state is the “torn down on <date>” signal.
  • Never read by roksbnkctl itself. The snapshot is an output for the user — never an input the tool depends on. Making it an input would create a feedback loop where redacted values get written back as the literal string <redacted>.

Redaction

Exactly one variable is redacted: ibmcloud_api_key. It’s the only var whose value comes from the cred resolver rather than being authored by the user in config.yaml or a tfvars file — so it’s the only value the snapshot would expose that the user didn’t put there themselves. See PRD 04 §“Cred tmpfile-bind-mount pattern” for why the API key isn’t in tfvars in the first place. The redacted line carries an inline comment:

ibmcloud_api_key = "<redacted>"  # source: cred resolver, not persisted

For team-handoff scenarios (a teammate receives this file out-of-band and wants to re-create the workspace): replace the <redacted> value with the teammate’s own API key, or simply remove the ibmcloud_api_key line so the cred resolver supplies it from the teammate’s own environment (keychain, shell env, ~/.bluemix/api_key, etc.) at apply time. Every other line round-trips verbatim.

The file mode is 0600 regardless. The non-redacted contents (workspace identifiers, region, resource group, cluster name, tunable values) aren’t credential-grade secrets, but aren’t world-readable-grade either. Tight permissions are the cheap default.

What it’s not

  • Not an input to subsequent applies. The -var-file chain on the next apply is unchanged: config.yaml-derived → terraform.tfvars.user → phase overrides.
  • Not a record of Terraform defaults. If variable "foo" { default = "bar" } and the user never set foo, the snapshot omits foo entirely. Capturing defaults would require running terraform output against the variables block — separate concern.
  • Not a state-derived value capture. Computed expressions, resource references, locals, and data-source values aren’t var-file inputs and don’t appear. terraform console against the live state dir is the right tool for those.
  • Not a TF_VAR_* env capture. roksbnkctl doesn’t set TF_VAR_* today — everything goes via -var-file — so the snapshot covers the complete input surface. A future cycle that starts using TF_VAR_* will need to extend this file.

Safe-to-commit guidance

The file is suitable for git commit alongside config.yaml after the user verifies the redaction matches their threat model. The standard reminder applies: the workspace dir may contain other semi-sensitive material — cluster-outputs.json records the cluster’s crn and admin identity hints; the state/ and state-cluster/ trees include terraform.tfstate (which contains resource IDs, IAM bindings, and any value Terraform’s provider exposed); the kubeconfig files are mode 0600 for a reason. Review the whole workspace dir with the same lens before committing.

roksbnkctl does not touch .gitignore. If you commit the workspace, you commit the workspace; if you don’t, you don’t. The tool stays out of that decision.

Worked example

For a ShapeSplit cluster phase apply, ~/.roksbnkctl/canada-roks/state-cluster/terraform.applied.tfvars looks like:

# Generated by roksbnkctl v1.4.0 at 2026-05-14T10:23:17Z after terraform apply on phase=cluster.
# Re-generated each apply. Do not edit by hand — your changes will be overwritten.

# === from config.yaml ===
cluster_name = "canada-roks"
ibmcloud_api_key = "<redacted>"  # source: cred resolver, not persisted
region = "ca-tor"
resource_group_name = "default"

# === from terraform.tfvars.user ===
worker_count = 4

# === from cluster-phase override ===
deploy_bnk = false

Re-applying from this snapshot alone reconstructs the inputs the user wrote; embedded Terraform module defaults are not captured (see §“What it’s not above for the full list of what’s out of scope).

The header records the binary version and apply timestamp so the reader can correlate the snapshot to a specific roksbnkctl invocation. Alphabetic ordering within each section means re-running apply with identical inputs produces a byte-identical file (idempotency — handy for diffing snapshots across applies).

The everyday workspace routine

The minimum daily routine:

# Initialise (creates ~/.roksbnkctl/<name>/config.yaml; defaults to "default")
roksbnkctl init

# Switch which workspace is "current"
roksbnkctl ws use prod

# See all workspaces and which one is current
roksbnkctl ws list

roksbnkctl init -w <name> is the one-shot path that creates the directory and populates config.yaml interactively. Everything else (ws new, ws use, ws delete) is the deconstructed form for users who want finer-grained control.

The full command tree

roksbnkctl workspaces ...     # canonical name
roksbnkctl ws ...              # alias

ws new <name> — empty skeleton

Creates ~/.roksbnkctl/<name>/ with no config.yaml. Useful when you want the directory to exist (so ws use works) before you run init.

roksbnkctl ws new staging
# ✓ Created workspace "staging" (run `roksbnkctl init -w staging` to configure)

Most users skip this and use roksbnkctl init -w staging directly, which does both steps in one go.

ws use <name> — switch current

Sets the current_workspace pointer in ~/.roksbnkctl/config.yaml:

roksbnkctl ws use prod
# ✓ Current workspace: prod

roksbnkctl ws current
# prod

Refuses to point at a non-existent workspace. The pointer is the only thing that changes — workspace state stays put.

ws current — print the pointer

roksbnkctl ws current
# default

Prints the current workspace name on stdout. If no pointer is set, prints a hint like “no current workspace; run roksbnkctl ws use <name> or roksbnkctl init” to stderr and exits 0 with empty stdout — so WS=$(roksbnkctl ws current) produces an empty string in scripts rather than spurious output.

ws list — table view

roksbnkctl ws list
NAME      CURRENT  REGION    CLUSTER          TF SOURCE
default   *        us-south  bnk-quickstart   embedded@v1.0.0
prod               eu-de     bnk-prod         embedded@v1.0.0
staging            us-south  bnk-staging      local:./terraform

The * marker on CURRENT highlights the active workspace. Other columns reflect each workspace’s config.yaml. Rows where config.yaml is missing or unparseable still show the name, with the other columns blank — the list never errors out because of one corrupt workspace.

ws delete <name> [--force]

Removes the workspace directory and the OS-keychain entry for its API key. Two safety rails:

  1. Refuses to delete the current workspace. You’d be left with a dangling current_workspace pointer, so delete errors out with: cannot delete current workspace "foo"; switch first: roksbnkctl ws use <other>.
  2. Refuses if Terraform state lists provisioned resources (unless --force). Catches the foot-gun where you forget to run roksbnkctl down first.
roksbnkctl ws delete staging
# Delete workspace "staging"? [y/N]: y
# ✓ Deleted workspace "staging"

# Refused — state still has resources
roksbnkctl ws delete prod
# Error: terraform state lists 77 resources; run `roksbnkctl down` first or pass --force

# I really mean it
roksbnkctl ws delete prod --force
# ✓ Deleted workspace "prod"

--force skips both the prompt and the state-non-empty check. Use it sparingly — there’s no “undo” for rm -rf ~/.roksbnkctl/<name>/.

The current-workspace pointer

The pointer lives at ~/.roksbnkctl/config.yaml:

current_workspace: prod

Every command that doesn’t pass -w reads this pointer. roksbnkctl init writes it on first run (so the very first init makes default current automatically). ws use rewrites it. Nothing else touches it.

If the pointer references a workspace that doesn’t exist (e.g. someone rm -rf’d the directory by hand), roksbnkctl errors out with a clear message: workspace "prod" referenced by current_workspace does not exist; run roksbnkctl ws use <other>.

-w / --workspace for one-off overrides

Every command accepts -w <name> to override the current pointer for a single invocation:

# Doctor against "prod" without flipping the global pointer
roksbnkctl -w prod doctor

# Run init for a new workspace called "staging"
roksbnkctl init -w staging

# Get pods from the "default" cluster while currently on "prod"
roksbnkctl -w default k get pods -A

Use this when:

  • You’re scripting against multiple workspaces in a single run (CI runner that exercises default + e2e-cleanup back-to-back).
  • You want to run a one-off command against a different environment without losing your current context.
  • You’re testing a fresh workspace before promoting it to current.

The flag only affects the running command — the pointer in ~/.roksbnkctl/config.yaml is unchanged. After the command exits, the next bare roksbnkctl reads the original pointer.

The parking-lot pattern

A subtle gotcha: ws delete refuses to remove the current workspace, but the end-to-end test suite needs to clean itself up after running against the default workspace.

The fix is the parking-lot pattern: have a throwaway workspace that exists only to be the “current” pointer while you delete other workspaces.

# End-to-end test cleanup (e2e-test.sh: Phase D destroys; Phase H runs the parking-lot dance below)

# Run the destroy against "default" (still current at this point)
roksbnkctl down --auto

# Park the pointer somewhere harmless
roksbnkctl ws new e2e-cleanup
roksbnkctl ws use e2e-cleanup

# Now we can drop the original workspace — it's no longer current
roksbnkctl ws delete default --force

# Optional: remove the parking lot too, by parking somewhere else first
roksbnkctl ws new tmp-park
roksbnkctl ws use tmp-park
roksbnkctl ws delete e2e-cleanup --force
roksbnkctl ws delete tmp-park --force   # leaves no current pointer

The pattern works because current_workspace only matters for commands that read workspace config. Once the pointer points elsewhere, the original workspace is just a directory and delete is happy to remove it.

If you want to delete every workspace including the parking lot, the last delete will leave you with an empty current_workspace. The next roksbnkctl init will populate it again with default.

Using a workspace’s environment in your shell

roksbnkctl shell drops you into a subshell with KUBECONFIG, IBMCLOUD_API_KEY, IC_API_KEY, and IBMCLOUD_REGION pre-loaded from the current workspace:

roksbnkctl shell
# (now in a subshell)
echo $KUBECONFIG
# /home/you/.roksbnkctl/default/state/kubeconfig
exit
# (back to the parent shell)

Same for -w:

roksbnkctl -w prod shell

Useful when you want to run host kubectl / host oc / arbitrary tools with the workspace context loaded. The Sprint 2 internalised verbs (roksbnkctl k get, etc.) read the same context automatically — you don’t need to be in a subshell to use them.

Common workspace patterns

A handful of patterns that come up in practice:

Use casePattern
Different IBM Cloud accountsdefault for personal, acct-foo for an account-specific key
Different regionsus-south, eu-de workspaces with distinct cluster.name values
Throwaway short-lived clustersbnk-trial-N workspaces; delete with --force after down
CI vs local devdev and ci workspaces; ci uses IBMCLOUD_API_KEY from env, dev uses keychain
Parking-lot cleanupe2e-cleanup workspace per “the parking-lot pattern” above

Workspaces are cheap. If a flow benefits from isolation, make a new one rather than fighting with --var-file overrides on the existing one.

This chapter covers the workspace-as-a-unit: how to create, switch, list, delete. The schema of the per-workspace config.yaml itself — every field, default, valid range — is Chapter 12 — Workspace config.