Kubernetes Clusters via API
Create, connect to, and scale a Kubernetes cluster with the Hyperstack API.
Hyperstack provisions on-demand Kubernetes clusters from a single API call. You choose a Kubernetes version, a flavor for the worker nodes, and a CPU-only flavor for the control plane, and Hyperstack provisions every node, installs Kubernetes, and returns a kubeconfig you can hand straight to kubectl.
This page walks the core cluster operations: create a cluster, connect to it, scale it, and manage its lifecycle. Every one of these operations is also available in the console. To work through the same tasks in the browser, see Kubernetes Clusters via UI. For per-endpoint parameters, schemas, and error codes, see the Clusters API reference.
How a cluster is assembled
A cluster is a set of virtual machines that Hyperstack provisions and wires together for you. Understanding the four node roles makes the API parameters below self-explanatory:
- Master nodes run the Kubernetes control plane. They take a CPU-only flavor (
n1-cpu-small,n1-cpu-medium, orn1-cpu-large), and every cluster runs 2 or 3 of them. - Worker nodes run your workloads and take any flavor, including GPU flavors. They are grouped into node groups that share a single flavor. A group named
defaultis created for you. - Bastion and load balancer nodes provide SSH access and route external traffic. They are provisioned in
fulldeployment mode and omitted instandardmode.
You never choose a node image. Hyperstack selects the image that matches your kubernetes_version, so every node in the cluster runs a consistent Kubernetes build. A cluster keeps the version it was deployed with, so to move to a newer version you deploy a new cluster and migrate your workloads.
Only worker nodes and public IP addresses are billed. Master, bastion, and load balancer nodes incur no resource-based charges, and billing for any node begins only once its underlying virtual machine reaches the ACTIVE state. See Cluster Billing & Data Retention.
Deploy and connect to a cluster via the API
Go from an empty environment to a cluster you can schedule workloads on. You create the cluster with one request, poll it until it reports ACTIVE, then read its kubeconfig from the same endpoint and hand it to kubectl.
Before you begin
You'll need:
- A Hyperstack account with billing activated and credit added.
- An API key, which authenticates every request below.
- An environment in a region that supports clusters, and an SSH keypair in that environment.
- kubectl and jq installed locally.
Clusters deploy in the CANADA-1 and NORWAY-1 regions. The US-1 region is not currently supported for cluster deployment.
Creating and scaling clusters is permission-gated. If a request fails because your account lacks permission, ask an organization admin to assign you a role that grants cluster management. See User Roles.
Set your API key in the shell so the snippets below work as-is. Replace your-api-key-here with the key you generated:
export HYPERSTACK_API_KEY="your-api-key-here"
Step 1: Create a cluster
- cURL
- Python
- Node.js
-
Choose a Kubernetes version and master flavor
Supported versions are region-scoped, so list the cluster versions and pick one available in your environment's region:
curl https://infrahub-api.nexgencloud.com/v1/core/clusters/versions \
-H "api_key: $HYPERSTACK_API_KEY"Then list the master flavors to see the CPU-only flavors available for the control plane:
curl https://infrahub-api.nexgencloud.com/v1/core/clusters/master-flavors \
-H "api_key: $HYPERSTACK_API_KEY" -
Create the cluster
Create the cluster with the request body below. Replace
your-environmentandyour-keypairwith your own names.kubernetes_version: a version returned by the versions endpoint above, available in your environment's regionmaster_flavor_name: a CPU-only flavor for the control planemaster_count: number of control-plane nodes, either2or3node_flavor_nameandnode_count: the flavor and size of thedefaultworker node group. To run several groups on different flavors, send anode_groupsarray instead (see the endpoint reference)deployment_mode:fulladds a bastion and a load balancer,standardprovisions master and worker nodes only. Defaults tofull
curl https://infrahub-api.nexgencloud.com/v1/core/clusters \
-H "api_key: $HYPERSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "example-cluster",
"environment_name": "your-environment",
"keypair_name": "your-keypair",
"kubernetes_version": "1.36.1",
"master_flavor_name": "n1-cpu-small",
"master_count": 2,
"node_flavor_name": "n3-A100x1",
"node_count": 1,
"deployment_mode": "full"
}'The response returns the cluster
idand aCREATINGstatus. Key fields shown below; the full response also includeskeypair_name,created_at,green_status, and per-nodeinstanceobjects:Response{
"status": true,
"message": "Cluster created successfully",
"cluster": {
"id": 1838,
"name": "example-cluster",
"environment_name": "your-environment",
"environment_region": "CANADA-1",
"kubernetes_version": "1.36.1",
"status": "CREATING",
"api_address": null,
"kube_config": null,
"master_count": 2,
"master_flavor": { "name": "n1-cpu-small", "cpu": 4, "ram": 4.0, "disk": 100 },
"image": { "name": "Ubuntu 24.04 LTS Hyperstack Kubernetes v1.36.1" },
"node_groups": [
{
"id": 1233,
"name": "default",
"role": "worker",
"flavor": { "name": "n3-A100x1", "gpu": "A100-80G-PCIe", "gpu_count": 1 },
"count": 1
}
],
"nodes": [
{ "id": 9549, "role": "bastion", "status": "CREATING" },
{ "id": 9550, "role": "load-balancer", "status": "CREATING" },
{ "id": 9551, "role": "master", "status": "CREATING" },
{ "id": 9552, "role": "master", "status": "CREATING" },
{ "id": 9553, "role": "worker", "status": "CREATING" }
]
}
}Note the
id. Substitute it for<cluster_id>in the steps that follow. -
Wait for the cluster to become
ACTIVERetrieve the cluster by ID and check its
status:curl https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id> \
-H "api_key: $HYPERSTACK_API_KEY"The cluster moves through
CREATINGwhile its virtual machines are provisioned, thenRECONCILINGwhile Kubernetes is installed and configured, and finallyACTIVE. Re-run the request until it reportsACTIVE. Larger clusters and larger GPU flavors take longer.Once the cluster is
ACTIVE,api_addressholds the Kubernetes API endpoint andkube_configholds the kubeconfig you need in Step 2.
-
Choose a Kubernetes version and master flavor
Supported versions are region-scoped, so list the cluster versions and pick one available in your environment's region, then list the master flavors to see the CPU-only flavors available for the control plane:
import os
import requests
base = "https://infrahub-api.nexgencloud.com/v1"
headers = {"api_key": os.environ["HYPERSTACK_API_KEY"]}
versions = requests.get(f"{base}/core/clusters/versions", headers=headers).json()
master_flavors = requests.get(f"{base}/core/clusters/master-flavors", headers=headers).json() -
Create the cluster
Create the cluster with the request body below. Replace
your-environmentandyour-keypairwith your own names.kubernetes_version: a version returned by the versions endpoint above, available in your environment's regionmaster_flavor_name: a CPU-only flavor for the control planemaster_count: number of control-plane nodes, either2or3node_flavor_nameandnode_count: the flavor and size of thedefaultworker node group. To run several groups on different flavors, send anode_groupsarray instead (see the endpoint reference)deployment_mode:fulladds a bastion and a load balancer,standardprovisions master and worker nodes only. Defaults tofull
cluster = requests.post(
f"{base}/core/clusters",
headers=headers,
json={
"name": "example-cluster",
"environment_name": "your-environment",
"keypair_name": "your-keypair",
"kubernetes_version": "1.36.1",
"master_flavor_name": "n1-cpu-small",
"master_count": 2,
"node_flavor_name": "n3-A100x1",
"node_count": 1,
"deployment_mode": "full",
},
).json()
cluster_id = cluster["cluster"]["id"]The response returns the cluster
idand aCREATINGstatus. Key fields shown below; the full response also includeskeypair_name,created_at,green_status, and per-nodeinstanceobjects:Response{
"status": true,
"message": "Cluster created successfully",
"cluster": {
"id": 1838,
"name": "example-cluster",
"environment_name": "your-environment",
"environment_region": "CANADA-1",
"kubernetes_version": "1.36.1",
"status": "CREATING",
"api_address": null,
"kube_config": null,
"master_count": 2,
"master_flavor": { "name": "n1-cpu-small", "cpu": 4, "ram": 4.0, "disk": 100 },
"image": { "name": "Ubuntu 24.04 LTS Hyperstack Kubernetes v1.36.1" },
"node_groups": [
{
"id": 1233,
"name": "default",
"role": "worker",
"flavor": { "name": "n3-A100x1", "gpu": "A100-80G-PCIe", "gpu_count": 1 },
"count": 1
}
],
"nodes": [
{ "id": 9549, "role": "bastion", "status": "CREATING" },
{ "id": 9550, "role": "load-balancer", "status": "CREATING" },
{ "id": 9551, "role": "master", "status": "CREATING" },
{ "id": 9552, "role": "master", "status": "CREATING" },
{ "id": 9553, "role": "worker", "status": "CREATING" }
]
}
}The code stores the
idascluster_id; the steps that follow use it automatically. -
Wait for the cluster to become
ACTIVEPoll the cluster detail endpoint until
statusreportsACTIVE:import time
while True:
detail = requests.get(f"{base}/core/clusters/{cluster_id}", headers=headers).json()
cluster = detail["cluster"]
if cluster["status"] == "ACTIVE":
break
time.sleep(30)
print(f"Cluster is ACTIVE at {cluster['api_address']}")The cluster moves through
CREATINGwhile its virtual machines are provisioned, thenRECONCILINGwhile Kubernetes is installed and configured, and finallyACTIVE. Larger clusters and larger GPU flavors take longer.Once the cluster is
ACTIVE,api_addressholds the Kubernetes API endpoint andkube_configholds the kubeconfig you need in Step 2.
-
Choose a Kubernetes version and master flavor
Supported versions are region-scoped, so list the cluster versions and pick one available in your environment's region, then list the master flavors to see the CPU-only flavors available for the control plane:
const base = "https://infrahub-api.nexgencloud.com/v1";
const headers = {
api_key: process.env.HYPERSTACK_API_KEY,
"Content-Type": "application/json",
};
const versions = await fetch(`${base}/core/clusters/versions`, { headers }).then((r) => r.json());
const masterFlavors = await fetch(`${base}/core/clusters/master-flavors`, { headers }).then((r) => r.json()); -
Create the cluster
Create the cluster with the request body below. Replace
your-environmentandyour-keypairwith your own names.kubernetes_version: a version returned by the versions endpoint above, available in your environment's regionmaster_flavor_name: a CPU-only flavor for the control planemaster_count: number of control-plane nodes, either2or3node_flavor_nameandnode_count: the flavor and size of thedefaultworker node group. To run several groups on different flavors, send anode_groupsarray instead (see the endpoint reference)deployment_mode:fulladds a bastion and a load balancer,standardprovisions master and worker nodes only. Defaults tofull
const created = await fetch(`${base}/core/clusters`, {
method: "POST",
headers,
body: JSON.stringify({
name: "example-cluster",
environment_name: "your-environment",
keypair_name: "your-keypair",
kubernetes_version: "1.36.1",
master_flavor_name: "n1-cpu-small",
master_count: 2,
node_flavor_name: "n3-A100x1",
node_count: 1,
deployment_mode: "full",
}),
}).then((r) => r.json());
const clusterId = created.cluster.id;The response returns the cluster
idand aCREATINGstatus. Key fields shown below; the full response also includeskeypair_name,created_at,green_status, and per-nodeinstanceobjects:Response{
"status": true,
"message": "Cluster created successfully",
"cluster": {
"id": 1838,
"name": "example-cluster",
"environment_name": "your-environment",
"environment_region": "CANADA-1",
"kubernetes_version": "1.36.1",
"status": "CREATING",
"api_address": null,
"kube_config": null,
"master_count": 2,
"master_flavor": { "name": "n1-cpu-small", "cpu": 4, "ram": 4.0, "disk": 100 },
"image": { "name": "Ubuntu 24.04 LTS Hyperstack Kubernetes v1.36.1" },
"node_groups": [
{
"id": 1233,
"name": "default",
"role": "worker",
"flavor": { "name": "n3-A100x1", "gpu": "A100-80G-PCIe", "gpu_count": 1 },
"count": 1
}
],
"nodes": [
{ "id": 9549, "role": "bastion", "status": "CREATING" },
{ "id": 9550, "role": "load-balancer", "status": "CREATING" },
{ "id": 9551, "role": "master", "status": "CREATING" },
{ "id": 9552, "role": "master", "status": "CREATING" },
{ "id": 9553, "role": "worker", "status": "CREATING" }
]
}
}The code stores the
idasclusterId; the steps that follow use it automatically. -
Wait for the cluster to become
ACTIVEPoll the cluster detail endpoint until
statusreportsACTIVE:let cluster;
while (true) {
const detail = await fetch(`${base}/core/clusters/${clusterId}`, { headers }).then((r) => r.json());
cluster = detail.cluster;
if (cluster.status === "ACTIVE") break;
await new Promise((r) => setTimeout(r, 30000));
}
console.log(`Cluster is ACTIVE at ${cluster.api_address}`);The cluster moves through
CREATINGwhile its virtual machines are provisioned, thenRECONCILINGwhile Kubernetes is installed and configured, and finallyACTIVE. Larger clusters and larger GPU flavors take longer.Once the cluster is
ACTIVE,api_addressholds the Kubernetes API endpoint andkube_configholds the kubeconfig you need in Step 2.
Step 2: Connect to your cluster
The cluster's kube_config field holds a base64-encoded kubeconfig file. Decode it, point KUBECONFIG at it, and kubectl talks to your cluster.
-
Retrieve and decode the kubeconfig
Read
kube_configfrom the cluster detail response and decode it into a local file. Replace<cluster_id>with your cluster's ID:B64_KUBECONFIG=$(curl -s https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id> \
-H "api_key: $HYPERSTACK_API_KEY" | jq -r '.cluster.kube_config')
echo "$B64_KUBECONFIG" | base64 -d > kubeconfig.yamlAny HTTP client works here. The field is populated on the same cluster-detail response you polled in Step 1.
-
Point
kubectlat the kubeconfigexport KUBECONFIG=$PWD/kubeconfig.yaml -
Verify the connection
kubectl get nodesEach master and worker node reports
Ready:OutputNAME STATUS ROLES AGE VERSION
example-cluster-default-0 Ready worker 5m v1.36.1
example-cluster-master-0 Ready control-plane 6m v1.36.1
example-cluster-master-1 Ready control-plane 5m v1.36.1Bastion and load balancer nodes do not appear here. They support the cluster's access and traffic paths rather than running workloads, so they are not registered as Kubernetes nodes.
If kubectl cannot reach the cluster, confirm the cluster reports ACTIVE and review the official Kubernetes troubleshooting guide.
You can also retrieve the kubeconfig through Terraform. See Creating Kubernetes clusters for a full example.
Scale your cluster
Add or remove nodes to match your workload. Both worker and master nodes can be added to a running cluster, within the limits below.
- Each node group holds between 1 and 20 worker nodes, and every cluster keeps at least one worker node.
- Every cluster keeps 2 or 3 master nodes.
- All existing nodes must be
ACTIVEbefore you add or remove a node, and scaling operations run one at a time.
While a node is added or removed the cluster reports RECONCILING, and existing nodes may briefly report RECONCILING or WAITING. Their virtual machines stay ACTIVE throughout, so running workloads are not interrupted.
Add and remove nodes
- cURL
- Python
- Node.js
-
List the cluster's nodes
Retrieve the cluster's nodes to get each node's
id,role, andstatus:curl https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id>/nodes \
-H "api_key: $HYPERSTACK_API_KEY" -
Add nodes
Add one or more nodes by specifying the
role. Useworkerwith anode_groupto grow a worker group, ormasterto add a control-plane node:curl -X POST https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id>/nodes \
-H "api_key: $HYPERSTACK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"role": "worker",
"count": 1,
"node_group": "default"
}'Response{
"status": true,
"message": "WORKER Node Created Successfully",
"nodes": [
{
"id": 9554,
"node_group_name": "default",
"role": "worker",
"status": "CREATING",
"instance": { "id": 923594, "name": "example-cluster-default-1", "status": "CREATING" }
}
]
} -
Remove a node
Drain the node from Kubernetes first, so its workloads reschedule onto the remaining nodes:
kubectl drain example-cluster-default-1 --ignore-daemonsets --delete-emptydir-data
kubectl delete node example-cluster-default-1Then delete the node using its
idfrom the node list:curl -X DELETE https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id>/nodes/<node_id> \
-H "api_key: $HYPERSTACK_API_KEY"Response{
"status": true,
"message": "Nodes [9554] are being deleted."
}To remove several nodes in one request, delete multiple cluster nodes by passing an array of node IDs.
-
List the cluster's nodes
Retrieve the cluster's nodes to get each node's
id,role, andstatus:nodes = requests.get(f"{base}/core/clusters/{cluster_id}/nodes", headers=headers).json()["nodes"] -
Add nodes
Add one or more nodes by specifying the
role. Useworkerwith anode_groupto grow a worker group, ormasterto add a control-plane node:added = requests.post(
f"{base}/core/clusters/{cluster_id}/nodes",
headers=headers,
json={"role": "worker", "count": 1, "node_group": "default"},
).json()
node_id = added["nodes"][0]["id"]Response{
"status": true,
"message": "WORKER Node Created Successfully",
"nodes": [
{
"id": 9554,
"node_group_name": "default",
"role": "worker",
"status": "CREATING",
"instance": { "id": 923594, "name": "example-cluster-default-1", "status": "CREATING" }
}
]
} -
Remove a node
Drain the node from Kubernetes first, so its workloads reschedule onto the remaining nodes:
kubectl drain example-cluster-default-1 --ignore-daemonsets --delete-emptydir-data
kubectl delete node example-cluster-default-1Then delete the node using its
id:requests.delete(f"{base}/core/clusters/{cluster_id}/nodes/{node_id}", headers=headers).json()Response{
"status": true,
"message": "Nodes [9554] are being deleted."
}To remove several nodes in one request, delete multiple cluster nodes by passing an array of node IDs.
-
List the cluster's nodes
Retrieve the cluster's nodes to get each node's
id,role, andstatus:const { nodes } = await fetch(`${base}/core/clusters/${clusterId}/nodes`, { headers }).then((r) => r.json()); -
Add nodes
Add one or more nodes by specifying the
role. Useworkerwith anode_groupto grow a worker group, ormasterto add a control-plane node:const added = await fetch(`${base}/core/clusters/${clusterId}/nodes`, {
method: "POST",
headers,
body: JSON.stringify({ role: "worker", count: 1, node_group: "default" }),
}).then((r) => r.json());
const nodeId = added.nodes[0].id;Response{
"status": true,
"message": "WORKER Node Created Successfully",
"nodes": [
{
"id": 9554,
"node_group_name": "default",
"role": "worker",
"status": "CREATING",
"instance": { "id": 923594, "name": "example-cluster-default-1", "status": "CREATING" }
}
]
} -
Remove a node
Drain the node from Kubernetes first, so its workloads reschedule onto the remaining nodes:
kubectl drain example-cluster-default-1 --ignore-daemonsets --delete-emptydir-data
kubectl delete node example-cluster-default-1Then delete the node using its
id:await fetch(`${base}/core/clusters/${clusterId}/nodes/${nodeId}`, {
method: "DELETE",
headers,
}).then((r) => r.json());Response{
"status": true,
"message": "Nodes [9554] are being deleted."
}To remove several nodes in one request, delete multiple cluster nodes by passing an array of node IDs.
Any data stored on a deleted node is permanently lost and cannot be recovered. Drain the node before deleting it, and keep data that must survive a node on persistent volumes provisioned through the CSI driver.
Working with node groups
A node group is a set of worker nodes that share one flavor. Deploying a cluster with node_flavor_name and node_count creates a single group named default. To run a mix of hardware in one cluster, add more groups, each with its own flavor:
- Create a node group with a
name, aflavor_name, androle: worker. - List node groups or retrieve a node group to inspect a group's flavor and node count.
- Update a node group to change its configuration.
- Delete a node group once its nodes are no longer needed. Every cluster must keep at least one node group with at least one worker node.
Manage the cluster lifecycle
Review cluster events
Retrieve a cluster's events to see the actions performed on it, each with a timestamp, type, and reason. Events record cluster creation, node group creation, node additions and deletions, and the outcome of each reconciliation:
curl https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id>/events \
-H "api_key: $HYPERSTACK_API_KEY"
Reconcile a cluster
Reconciliation reapplies the intended configuration to a cluster. Hyperstack reconciles automatically during creation and scaling. You can trigger a reconciliation when a cluster is in an ERROR state or has stalled, provided it is not already RECONCILING:
curl -X POST https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id>/reconcile \
-H "api_key: $HYPERSTACK_API_KEY"
If reconciliation does not resolve the issue, delete the cluster and create a new one rather than retrying. See Cluster Reconciliation.
Delete a cluster
Clusters do not support hibernation and are billed until deleted. To stop all charges, delete the cluster:
curl -X DELETE https://infrahub-api.nexgencloud.com/v1/core/clusters/<cluster_id> \
-H "api_key: $HYPERSTACK_API_KEY"
{
"status": true,
"message": "Cluster example-cluster is being deleted"
}
Deletion is permanent and includes all data not stored externally. To reduce cost without losing the cluster, remove worker nodes instead. See Cluster Billing & Data Retention.
Whitelisting worker node IP addresses
Third-party services that restrict access by IP address need the public addresses your worker nodes use for outbound traffic. Worker nodes carry no public IP of their own, so their outbound traffic leaves through a different address than the bastion and load balancer public IPs. Retrieve it by opening a debug container on a worker node and querying an external echo service:
# List the nodes in the cluster
kubectl get nodes
# Open a debug terminal on a worker node
kubectl debug node/example-cluster-default-0 -it --image=busybox
# Retrieve the public IP address
wget -qO- ifconfig.me/ip
The echo service returns the address on its own line:
203.0.113.42
Repeat for each worker node, then add every address returned to the third-party service's allowlist.
For the ports each node role requires, and the rules to apply when you expose a service, see Firewall Rules for Kubernetes Clusters.
Next steps
Resources
Clusters API reference
Every cluster, node, and node group endpoint with full parameters and schemas.
Clusters via the Console
Run the same operations in the browser, plus node types, statuses, and billing detail.
CSI Driver
Provision persistent volumes so data survives a node being replaced.
Configuring Ingress
Route external traffic to services running in your cluster.
Firewall Rules
Minimum required firewall rules for each cluster node role.
Deploy an LLM with vLLM
Run a large language model on your cluster with the vLLM inference framework.