frostmoln_kubernetes_cluster (Resource)
Manages a managed Kubernetes cluster in the Frostmoln platform. The cluster owns its initial node pool (created embedded, scaled in-place). Additional node pools are managed with the frostmoln_kubernetes_node_pool resource.
~> Terraform cannot see that an ATTACHED address depends on the VPC's gateway. This resource attaches one for the API endpoint, on accounts where it takes a public address, and an address reaches the outside world only through a gateway — but nothing that attaches an address refers to frostmoln_gateway, so nothing orders the two. Terraform runs them concurrently and either can win.
On teardown the gateway can go first, and its delete is then refused ("Gateway is still in use", GATEWAY_IN_USE) because something in the VPC still depends on it — the failure that stops a terraform destroy half way through. On create the attachment can land first, and the platform then attaches a gateway ITSELF to carry it: a frostmoln_gateway that names a public_ip_id is refused after that ("VPC already has a gateway", GATEWAY_EXISTS), and one that names none is not refused at all — it quietly ADOPTS the gateway the platform made, leaving the VPC egressing from whatever address that gateway already had rather than one this configuration names, with origin reading implicit_public_ip.
Where the same configuration manages the gateway, state the ordering yourself: put depends_on = [frostmoln_gateway.<name>] on the resource that makes the ATTACHMENT — this one. Where the gateway is in another module, the dependency is on the module itself: depends_on = [module.<name>]. This resource is not the only one that needs it: frostmoln_apache_instance (public), frostmoln_load_balancer (public_ip_id), frostmoln_nginx_instance (public), frostmoln_public_ip_association and frostmoln_public_ip (instance_id) attach addresses too, and each takes the line on itself.
It works only where the gateway is a frostmoln_gateway RESOURCE in the same configuration. A data "frostmoln_gateway" cannot carry the order — a data source is read, never created or destroyed — so depending on one defers a read and sequences nothing.
Do not write it the other way about — on the gateway, listing what attaches. depends_on orders the resource it is written on, so that reverses both orders and turns a race that sometimes passed into a teardown that fails every time.
It changes ORDER only: nothing is created and nothing is released. It does not arm the gateway's own destroy either — without acknowledge_connectivity_loss the teardown stops at that refusal instead, and never reaches the ordering at all. And if a gateway was already adopted, nothing needs importing or rebuilding: it is in state already — add the ordering so it cannot recur, then give the gateway the address you meant with public_ip_id, which is applied in place.
Example Usage
data "frostmoln_kubernetes_versions" "available" {}
data "frostmoln_kubernetes_flavors" "available" {}
# The depends_on is not decoration. A cluster attaches a public IP into your VPC
# for its API endpoint, on accounts where that endpoint takes one — and it does so
# whether or not you name an address of your own, allocating one for you when you
# name none. Nothing here refers to frostmoln_gateway, so Terraform runs the two
# concurrently and on teardown the gateway can go first, with its delete refused
# (GATEWAY_IN_USE) part-way through the destroy.
resource "frostmoln_kubernetes_cluster" "main" {
name = "my-cluster"
version = [for v in data.frostmoln_kubernetes_versions.available.versions : v.version if v.is_default][0]
vpc_id = frostmoln_vpc.main.id
subnet_id = frostmoln_subnet.nodes.id
depends_on = [frostmoln_gateway.main]
# To reach your workloads from outside the cluster, give your ingress controller
# (or any workload) a Kubernetes Service of type LoadBalancer: the platform
# provisions a load balancer per Service and reports its address in
# .status.loadBalancer.ingress. The cluster itself provisions none — `endpoint`
# below is the Kubernetes API endpoint, for kubectl, not for traffic.
# Cluster addons are installed once, at creation, and cannot be changed on an
# existing cluster (changing this set replaces the cluster). Omit the attribute
# to install the platform defaults; set an empty list ([]) to install none.
# See the frostmoln_kubernetes_addons data source for available keys.
addons = ["external-secrets"]
initial_node_pool = {
flavor_id = data.frostmoln_kubernetes_flavors.available.flavors[0].id
node_count = 3
}
}
output "cluster_endpoint" {
value = frostmoln_kubernetes_cluster.main.endpoint
}
output "kubeconfig" {
value = frostmoln_kubernetes_cluster.main.kubeconfig
sensitive = true
}Schema
Required
initial_node_pool(Attributes) The cluster's initial node pool, owned by this resource. (see below for nested schema)name(String) The name of the cluster. Updatable in-place.subnet_id(String) The subnet ID where the cluster nodes are deployed.vpc_id(String) The VPC ID where the cluster nodes are deployed.
Optional
addons(Set of String) The set of cluster-addon catalog keys to install at cluster creation (see the frostmoln_kubernetes_addons data source for available keys). Addons are applied ONCE, at cluster creation, from first-boot manifests — they cannot be changed on an existing cluster, so changing this set REPLACES the cluster. Leave it unset to apply the platform default addons (currently external-secrets); set it to an explicit empty set ([]) to install no addons.control_plane_tier(String) The control-plane tier key (see the frostmoln_kubernetes_tiers data source for canonical keys). Defaults to the platform default tier.public_ip_id(String) The ID of an existing public IP to use for the cluster API endpoint (bring-your-own public IP). Write-only on the API: reads expose only the resolved address (public_ip), so imports cannot recover this value — after importing a cluster created with a BYO public IP, omit this attribute or addlifecycle { ignore_changes = [public_ip_id] }, otherwise the next plan will want to replace the cluster. A BYO public IP survives cluster deletion. This is the API endpoint's address, and the only one this resource takes: the cluster no longer provisions a worker ingress load balancer — expose workloads with a Service of type LoadBalancer, which gets its own.
Where this attribute is accepted at all, the address lands on a port in your own VPC and carries the same gateway dependency as every other attachment — see the ordering note on this resource above. On some accounts this attribute is refused outright (400) — the cluster API endpoint takes no address of yours there — so if you get that refusal, the ordering it describes does not arise for the API endpoint.
region(String) The region to create the cluster in. Defaults server-side.version(String) The Kubernetes version (e.g. "1.35"). Defaults to the platform default version. Changing it currently REPLACES the cluster — in-place upgrade is not available yet.
Read-Only
ca_cert_hash(String) The cluster CA certificate hash.created_at(String) The timestamp when the cluster was created.endpoint(String) The Kubernetes API endpoint URL.ha_enabled(Boolean) Whether the control plane is highly available (derived from the control-plane tier).id(String) The unique identifier of the cluster.kubeconfig(String, Sensitive) A kubeconfig for the cluster. Stored in the Terraform state in plaintext — protect the state file accordingly.load_balancer_id(String) The ID of the load balancer fronting the Kubernetes API.pod_cidr(String) The server-allocated pod network CIDR.public_ip(String) The public IP address of the cluster API endpoint.service_cidr(String) The server-allocated service network CIDR.status(String) The current status of the cluster.tenant_id(String) The tenant ID that owns this cluster.updated_at(String) The timestamp when the cluster was last updated.
Nested Schema for initial_node_pool
Required:
flavor_id(String) The node flavor ID (see the frostmoln_kubernetes_flavors data source).
Optional:
name(String) The name of the initial node pool. Defaults to "default".node_count(Number) The number of worker nodes in the initial pool (1-100). Scaled in-place. Omitting it manages the pool at 1 node — an out-of-band scale will be reverted on the next apply.
Read-Only:
id(String) The unique identifier of the initial node pool.status(String) The current status of the initial node pool.
Import
Import is supported using the following syntax:
The terraform import command can be used, for example:
# Import a cluster by its ID. public_ip_id cannot be recovered (write-only
# on the API) — after import, omit it or add
# lifecycle { ignore_changes = [public_ip_id] } to avoid a replacement plan.
terraform import frostmoln_kubernetes_cluster.main 51455a51-db3d-4231-ac27-4fee2553c15f