# Deployment spaces

> How ZopDay represents a Kubernetes cluster; registered, probed, and addressable.

Source: https://zop.dev/docs/zopday/concepts/deployment-spaces

---

A **deployment space** is a registered Kubernetes cluster on a connected cloud account. It's the unit of "where things run." Environments bind to spaces; deployments push manifests through spaces; the autoscaler scales targets inside spaces. Everything cluster-shaped goes through this abstraction.

![Deployment Spaces list showing five active clusters across GCP, AWS, and Azure; us-prod and us-staging on GCP us-central1, eu-prod and eu-staging on AWS eu-west-1, and apac-prod on Azure southeastasia](https://storage.googleapis.com/zopdev-blog-resources/1/files/originals/20260602/cfca1690-dcfe-466c-9b85-2c232cc9f003-zopdayspacesenterprise.png)

*Deployment Spaces; registered clusters across GCP, AWS, and Azure with region and health status.*

## Anatomy of a space

A space carries:

| Field | What it is |
|---|---|
| `id` | UUID. Stable across renames / re-registrations. |
| `org_id` | The org that owns the registration. |
| `provider` | `aws` · `gcp` · `azure` |
| `space_type` | `kubernetes` today; future types reserved. |
| `cloud_account_id` | The connected cloud account ZopDay authenticates against. |
| `config` JSON | Provider-specific identifiers (cluster name, region, resource group on Azure). |
| `status` | `active` · `inactive` · `unknown` from the 60-second reachability probe. |

Spaces are not the cluster itself; they are ZopDay's pointer to the cluster + the kubeauth permission to talk to it.

## Provider-specific config

The `config` JSON varies per provider because each cloud identifies clusters differently:

```json AWS
{
  "clusterName": "prod-eks-1",
  "region": "us-east-1"
}
```

```json GCP
{
  "clusterName": "prod-gke-1",
  "region": "us-central1",
  "projectId": "acme-prod-12345"
}
```

```json Azure
{
  "clusterName": "prod-aks-1",
  "resourceGroup": "rg-prod-eus",
  "subscriptionId": "00000000-0000-0000-0000-000000000000"
}
```

These fields are not editable on an existing space; they're the cluster's identity, not its config. If you need to point at a different cluster, deregister and re-register.

## How spaces get created

Two paths:

**BYOC; existing cluster**

    Cluster already exists in your cloud. Connect the cloud account, pick the cluster, click Register. See [Cluster registration](https://zop.dev/docs/zopday/cloud-setup/cluster-registration).

**Provisioned; fresh cluster**

    Cluster doesn't exist yet. Provisioning job creates the cluster end-to-end (EKS / GKE / AKS) and registers it as a space automatically on success. See [Cluster provisioning](https://zop.dev/docs/zopday/provisioning/clusters).

Either path lands the same `deployment_space` row.

## Endpoint surface

Catalog operations live on the config service (`/spaces*`):

```
GET    /spaces                List
POST   /spaces                Add; register an existing cluster
GET    /spaces/{id}           Get one
PATCH  /spaces/{id}/status    Update status (internal use)
DELETE /spaces/{id}           Soft-delete (deregister)
```

Cluster-side operations that need a kubeconfig live under `/space-discovery/*`:

```
GET   /space-discovery/{spaceID}/namespaces   List existing namespaces on the cluster
POST  /space-discovery/{spaceID}/namespaces   Create a new namespace
```

**Note**

These are relative paths. See the API reference for the base URL, the `Authorization: Bearer` header, and full request/response examples.

## Multi-cluster orgs

Most ZopDay orgs end up with multiple spaces. Common patterns:

- **Per-environment**: one cluster per environment (`prod`, `staging`, `dev`). Highest isolation, highest cost.
- **Per-region**: one cluster per region for latency or compliance. Often paired with a global ingress in front.
- **Per-team / per-product**: one cluster per major team or product line. Useful when teams want full autonomy.
- **Shared dev + dedicated prod**: common cost-optimisation: a single shared dev cluster, dedicated prod clusters per environment.

ZopDay doesn't enforce any of these; the space picker in the environment wizard accepts any registered space, regardless of which other environments already point at it.

## Reachability probe

Every 60 seconds, ZopDay calls `GET /version` on each space's API server using a freshly-minted kubeconfig. The result updates `status`:

| Probe outcome | Status |
|---|---|
| 200 with parseable version | `active` |
| 3 consecutive failures | `inactive` |
| Less than 1 probe interval since registration | `unknown` |

Status drives gating in the UI:

- **Inactive spaces don't show up in the autoscaler picker, event-readiness picker, or environment picker**: they're filtered before render
- **Active deployments on an inactive space** continue running; ZopDay just can't push new revisions until it recovers
- **Queued revisions** retry up to 3 times against an inactive space, then stop. Re-enqueue from the Provisioning Jobs view once the space recovers.

## Synthetic vs real spaces

Some places in the UI need a "space-shaped" object that isn't a real cluster (topology grouping, picker placeholders). These synthetic entries are filtered out of the autoscaler picker, event-readiness picker, and any scaling-aware UI; those operate only on real, scalable clusters.

## Cascading effects of deregistration

Deregistering a space (`DELETE /spaces/{id}`):

| Object | Effect |
|---|---|
| Environments bound to this space | Gate cleanly with "Space deregistered"; re-bind to another space to recover |
| Pending deployment revisions | Move to DLQ on next claim attempt |
| Active deployments (already deployed) | Continue running on the cluster. ZopDay just can't push new revisions |
| Autoscaler policies targeting resources in this cluster | Stop being applied. The underlying cloud-side scaling policy remains as written before deregistration. |
| Kubeauth grant | Removed. ZopDay can no longer mint kubeconfigs for this cluster. |

Deregistration is reversible by re-registering the cluster; the `id` field on the new registration will be different (new UUID), so any environments that referenced the old `id` still need to be re-bound manually.
