Registering a Cluster
A cluster must be registered with kubenest before you can deploy applications to it. Registration is always a two-part process: creating a cluster record in the backend (which issues a cluster JWT), and installing the operator on the actual Kubernetes cluster (which uses that JWT to authenticate with the hub). How the Kubernetes cluster itself comes to exist is what separates the two registration paths.
Path 1: Manual registration (BYO cluster)
Bring-your-own-cluster (BYOC) registration is for clusters that already exist — whether that is a managed service (EKS, GKE, AKE), a self-managed kubeadm cluster, or a local k3s instance. kubenest does not need access to the cloud provider; you install the operator on the cluster yourself.
Create the cluster record
Cluster records are created under an organization, so you need your organization’s UUID. Fetch it from GET /api/v1/orgs if you do not already have it:
ORG_ID=$(curl -s -H "Authorization: Bearer $TOKEN" \
https://api.your-domain.com/api/v1/orgs | jq -r '.[0].id')Now create the cluster record. This reserves a UUID, mints the cluster JWT, and generates the Helm install command.
For a BYO cluster, omit provider and credential_id entirely — their presence is what tells kubenest to provision infrastructure for you. The request body rejects unknown fields, so do not send "provider": "manual"; there is no such provider.
curl -X POST "https://api.your-domain.com/api/v1/orgs/$ORG_ID/clusters" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "prod-cluster",
"description": "AWS EKS us-east-1 production cluster"
}'Response (201 Created):
{
"id": "a1b2c3d4-0001-0000-0000-000000000000",
"name": "prod-cluster",
"status": "pending",
"connection_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"install_command": "helm install kubenest-operator kubenesthq/kubenest-operator -n kubenest-system --create-namespace --set operator.hub.url=wss://hub.your-domain.com --set operator.hub.token=eyJhbGc... --set operator.cluster.id=a1b2c3d4-..."
}A manually registered cluster starts in pending and moves to connected once the operator’s WebSocket handshake reaches the hub. Cloud-provisioned clusters start in provisioning instead.
Save the cluster ID. You will need it in subsequent API calls:
CLUSTER_ID="a1b2c3d4-0001-0000-0000-000000000000"You can also retrieve the install command at any time via the API:
curl -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/clusters/$CLUSTER_ID/install-command"Note that this endpoint mints a new 365-day cluster JWT on every call and stores it as the cluster’s current token. If the operator is already running with an older token, it keeps working (see the note on revocation below), but the command you get back will not match the token in the running Secret. To read the install steps without minting a new token, use /install-instructions instead, which reuses the stored token when one exists.
Prepare your Helm values
For production installs, use a values file rather than a --set chain. This makes the configuration auditable and easier to update:
operator:
hub:
url: wss://hub.your-domain.com
token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... # from cluster record
cluster:
id: a1b2c3d4-0001-0000-0000-000000000000
gitops:
repoUrl: https://github.com/your-org/gitops-repo
branch: main
token: ghp_... # personal access token with repo:write scope
argocd:
enabled: false # assumes ArgoCD is already installed
namespace: argocdThe gitops.repoUrl must be a repository that:
- The operator has read/write access to via the provided token.
- Is not shared with other clusters unless you understand that they will write to different subtrees (see GitOps and Drift Detection).
Install the operator on the cluster
Make sure kubectl is pointing at the target cluster, then install:
helm repo add kubenesthq https://charts.kubenestapp.com
helm repo update
helm install kubenest-operator kubenesthq/kubenest-operator \
--namespace kubenest-system \
--create-namespace \
--values kubenest-operator-values.yamlWatch the pod start:
kubectl get pods -n kubenest-system -wExpected output once healthy:
NAME READY STATUS RESTARTS AGE
kubenest-operator-controller-manager-xyz 1/1 Running 0 30sCheck the operator log for the hub connection message:
kubectl logs -n kubenest-system \
-l app.kubernetes.io/name=kubenest-operator \
--tail=30Look for:
{"level":"info","msg":"connected to hub","cluster_id":"a1b2c3d4-...","hub":"wss://hub.your-domain.com"}Confirm the cluster is connected
Once the operator connects, the hub notifies the backend, which transitions the cluster status from pending to connected:
curl -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/clusters/$CLUSTER_ID" | jq .status
# "connected"The cluster is now ready to receive deployments. Create a project and deploy your first app — see Creating and Managing Apps.
Secure the cluster JWT. The connection token is a long-lived credential with the ability to execute any operation the operator supports on that cluster. The Helm chart stores it in a Kubernetes Secret (kubenest-operator-token in the kubenest-system namespace). Restrict access to that Secret using Kubernetes RBAC.
Issuing a new token does not revoke the old one. The hub accepts any correctly signed, unexpired cluster JWT — it does not check the token against the one currently stored for the cluster. A leaked token therefore stays usable for the remainder of its 365-day lifetime. If a token is compromised, treat rotating the hub’s JWT_SECRET (which invalidates every cluster token at once and requires re-issuing all of them) or deleting and re-registering the cluster as the only reliable containment.
To issue a fresh cluster JWT, call GET /api/v1/clusters/{id}/install-command. Every call mints a new 365-day token and stores it as the cluster’s current token; the response contains both the token and a ready-to-run Helm command. Update the kubenest-operator-token Secret with the new value and restart the operator pod.
Path 2: Cloud-provisioned clusters (AWS EC2)
kubenest can provision a Kubernetes cluster on AWS by creating EC2 instances and bootstrapping Kubernetes automatically. This path is appropriate when you do not have an existing cluster and want kubenest to manage the infrastructure lifecycle as well as the application lifecycle.
Cloud-provisioned clusters are currently supported on AWS EC2 only. Support for GCP GKE and Azure AKS is planned. The provisioner uses Terraform under the hood; you need a CloudCredential record with valid AWS credentials before proceeding.
Prerequisites
- Create a
CloudCredentialrecord with your AWS credentials:
Credential fields are flat, not nested — the payload rejects unknown fields.
curl -X POST "https://api.your-domain.com/api/v1/orgs/$ORG_ID/credentials" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "aws-prod",
"provider": "aws",
"region": "us-east-1",
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
}'- The credential ID from the response is used in the cluster create request.
provider accepts aws, gcp, azure, do, metal, and ssh. Credentials are accepted for all of them, but only aws is wired for provisioning today — the others return 422 with a “coming soon” message when you try to create a cluster with them.
Create a provisioned cluster
Provisioning options are flat fields on the cluster body, not a nested provision_config. Setting both provider and credential_id is what switches this from a BYO registration to a provisioning request.
curl -X POST "https://api.your-domain.com/api/v1/orgs/$ORG_ID/clusters" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "staging-aws",
"provider": "aws",
"credential_id": "cred-uuid-...",
"region": "us-east-1",
"instance_type": "t3.medium",
"agent_count": 3,
"disk_size_gb": 40
}'agent_count is the number of agent nodes (0–20) alongside the control-plane node, and disk_size_gb accepts 10–500. If you omit region, the credential’s own region is used. The Kubernetes version is not selectable — provisioned clusters run the k3s version baked into the provisioner image.
Status transitions
Cloud-provisioned clusters move through more states than BYOC registrations:
pending
│
▼
provisioning ← Terraform creates VPC, security groups, EC2 instances
│
▼
awaiting_operator ← VMs are up; k3s is bootstrapped; waiting for operator first connect
│
▼
connected ← Operator has authenticated; cluster is readyPoll or stream status:
curl -N -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/events/stream?cluster_id=$CLUSTER_ID"Provisioning typically takes 5–10 minutes depending on the region and instance count.
SSH access
The provisioner generates an SSH key pair during provisioning. The private key is stored in the backend database (encrypted), tied to the cluster record. Retrieve it if needed for troubleshooting:
curl -H "Authorization: Bearer $TOKEN" \
"https://api.your-domain.com/api/v1/clusters/$CLUSTER_ID/ssh-key" | jq -r .private_key > cluster.pem
chmod 600 cluster.pem
ssh -i cluster.pem ec2-user@<node-ip>The private key is only available to organization admins. Treat it with the same care as the cluster JWT.
Scaling a cloud-provisioned cluster
curl -X POST https://api.your-domain.com/api/v1/clusters/$CLUSTER_ID/scale \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"desired_node_count": 5}'kubenest runs a Terraform apply to add or remove EC2 instances and registers the new nodes with the existing Kubernetes control plane. BYOC clusters are not affected by the scale endpoint — node management for those clusters is handled outside kubenest.
Connection state reference
| State | Meaning | Possible next states |
|---|---|---|
pending | Cluster record created; no action taken | provisioning, awaiting_operator |
provisioning | Cloud provider creating infrastructure | awaiting_operator, error |
awaiting_operator | Infrastructure ready; operator not yet connected | connected, error |
connected | Operator has an active WebSocket session | disconnected |
disconnected | Hub lost the session; operator may be restarting | connected (auto-reconnect) |
error | Unrecoverable error; see message field | — |
The operator implements exponential backoff reconnection. Most disconnected states resolve within 30–60 seconds without user intervention.
Deleting a cluster
Deleting a cluster record from the backend does not automatically uninstall the operator or delete the Kubernetes cluster.
For BYOC clusters, clean up in this order:
# 1. Delete all projects on the cluster first
# (the API will reject cluster delete if projects exist)
# 2. Uninstall the operator from the cluster
helm uninstall kubenest-operator -n kubenest-system
# 3. Optionally remove the namespace
kubectl delete ns kubenest-system
# 4. Delete the cluster record from the backend
curl -X DELETE https://api.your-domain.com/api/v1/clusters/$CLUSTER_ID \
-H "Authorization: Bearer $TOKEN"For cloud-provisioned clusters, the backend runs a terraform destroy when you delete the cluster record, which tears down the EC2 instances, VPC, and associated resources. Make sure you have deleted all projects and apps first.
Troubleshooting
Operator pod is in CrashLoopBackOff. The most common cause is a malformed or expired cluster JWT. Check the pod logs for "invalid token" or "token expired". Issue a new token via GET /api/v1/clusters/{id}/install-command, update the kubenest-operator-token Secret, and restart the pod.
Cluster stuck in awaiting_operator after operator install. The operator is not reaching the hub. Verify:
- The pod is
Running(notCrashLoopBackOff). - The
operator.hub.urlvalue starts withwss://(notws://). - Outbound HTTPS/WSS traffic from the cluster to the hub URL is permitted by your network policy.
- DNS resolves the hub URL from inside the cluster:
kubectl exec -n kubenest-system <operator-pod> -- curl -I https://hub.your-domain.com.
connected then immediately disconnected. This is usually a heartbeat timeout. The hub expects a heartbeat from the operator every 30 seconds. Check for high CPU or memory pressure on the operator pod that might delay heartbeats. Adjust operator.heartbeat.intervalSeconds in the Helm values if your cluster is resource-constrained.
Provisioning stuck in provisioning for more than 20 minutes. Check the provisioner job logs on the backend pod:
kubectl logs -n kubenest-system \
-l app.kubernetes.io/name=kubenest-backend \
--tail=50 | grep provisionerCommon causes: AWS credentials lack the required IAM permissions, the requested instance type is unavailable in the region, or a VPC limit has been reached.
See also:
- Architecture Overview — how the operator, hub, and backend fit together and why the JWT model exists
- Installation — the full Helm values reference for the operator chart
- Projects — the next step after a cluster is connected