Skip to content
IaC Templates

Kubernetes Cluster Bootstrap

Template · Updated Jul 2026
Validated Jul 2026

Kubernetes cluster bootstrap

This pattern composes Compute, Network, and Kubernetes.

What this template does#

Bootstraps a Kubernetes cluster on Quake AI compute instances:

  • Control plane node(s) with kubeadm initialization
  • Worker nodes joined to the cluster
  • Dedicated private network for cluster communication
  • Security groups for Kubernetes API, etcd, kubelet, and NodePort ranges
  • Optional floating IP for external API access

This template provisions the infrastructure. Kubernetes installation is handled via cloud-init scripts embedded in the template. See cloud-init and first-boot configuration.

Parameters#

ParameterDescriptionDefault
key_nameSSH keypair name (must already exist in your project)required
control_plane_countNumber of control plane nodes1
worker_countNumber of worker nodes3
cp_flavorControl plane instance sizem2a.xlarge
worker_flavorWorker node instance sizes1a.medium
k8s_versionKubernetes version1.31
pod_cidrPod network CIDR10.244.0.0/16
image_nameOperating system imageUbuntu-24.04
external_networkExternal network for router gateway and floating IPPublicStatic
private_cidrAddress range for the cluster subnet192.168.70.0/24
dns_nameserversResolvers for the private subnet (cloud-init needs DNS to reach package mirrors)["8.8.8.8", "8.8.4.4"]

When to use this pattern#

Bootstrap a Magnum Kubernetes cluster with control plane and worker nodes on a private network. Choose compute-only patterns such as Containerized App on Compute when you want Docker on a single VM without cluster orchestration.

Estimated cost#

Monthly cost estimate

Pricing calculator ↗

Sized as a custom package on a mix of shared and dedicated vCPU.

Starting template$238.80/mo

Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.

What each resource is for

Control plane

m2a.xlarge · 4 dedicated vCPU, 16 GiB RAM, 1 Gbps

$132.00/mo

3× Worker node

s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$99.00/mo

Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.

Included in baseline

m2a.xlarge

4 dedicated vCPU, 16 GiB RAM, 1 Gbps

$132.00

s1a.medium

4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$33.00

s1a.medium

4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$33.00

s1a.medium

4 shared vCPU, 4 GiB RAM, 0.5 Gbps

$33.00

Compute + RAM rate basis

16 vCPU + 28 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.

—

Block storage (160 GiB)

160 GiB at $0.08/GiB/mo

$12.80

Public IP (included)

1 included with the custom package

$0.00

Package promotional discount

Flat −$5.00/mo on the custom package (same promotion as named plans).

$-5.00

Included at no charge

These line items are zero on Quake AI. Many other providers meter them separately.

Data transfer (inbound and outbound)

Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.

AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.

Learn more
$0.00

Private networking

Private networks, subnets, Neutron routers, and security groups are included with the plan.

VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.

$0.00

Control-plane API requests

OpenStack API calls for provisioning and management are included.

Some managed services on other clouds meter API calls or charge for premium control-plane features.

$0.00

Kubernetes control plane

Magnum clusters run on Nova instances; there is no separate K8s platform fee in Quake AI pricing.

Managed Kubernetes on AWS, GCP, and Azure charges a control-plane fee on top of worker nodes.

Learn more
$0.00

Dev/test vs production

Start on shared CPU for dev/test, then promote to dedicated for production with a flavor resize. The network, storage, and template stay the same.

Dev/test on shared CPU

Burstable s1a flavors; suited to prototyping and low or bursty load.

$139.80/mo

Production on the configured CPU

The headline estimate above; predictable steady-load performance.

$238.80/mo

Saves $99.00/mo while you build on shared CPU.

Shared flavors carry less RAM (m2a.xlarge (16 GiB RAM) -> s1a.medium (4 GiB RAM)). A resize reboots the instance; data on attached volumes persists. Size the dedicated flavor for the RAM your production workload needs.

Pricing data last validated: . For current rates, check quake.ai/pricing.

Template source#

8 files. Download the zip or expand to copy any file.Download k8s-cluster.zip
Show source (8 files)
main.tfHCL
data "openstack_images_image_v2" "os" {
  name        = var.image_name
  most_recent = true
}

data "openstack_networking_network_v2" "external" {
  name = var.external_network
}

resource "openstack_networking_network_v2" "private" {
  name           = "k8s-cluster-private"
  admin_state_up = true
}

resource "openstack_networking_subnet_v2" "private" {
  name            = "k8s-cluster-private-subnet"
  network_id      = openstack_networking_network_v2.private.id
  cidr            = var.private_cidr
  ip_version      = 4
  enable_dhcp     = true
  dns_nameservers = var.dns_nameservers
}

resource "openstack_networking_router_v2" "gw" {
  name                = "k8s-cluster-router"
  admin_state_up      = true
  external_network_id = data.openstack_networking_network_v2.external.id
}

resource "openstack_networking_router_interface_v2" "private" {
  router_id = openstack_networking_router_v2.gw.id
  subnet_id = openstack_networking_subnet_v2.private.id
}

resource "openstack_networking_secgroup_v2" "control_plane" {
  name        = "k8s-cluster-control-plane"
  description = "Kubernetes control plane"
}

resource "openstack_networking_secgroup_rule_v2" "cp_ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.control_plane.id
}

resource "openstack_networking_secgroup_rule_v2" "cp_api" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 6443
  port_range_max    = 6443
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.control_plane.id
}

resource "openstack_networking_secgroup_rule_v2" "cp_etcd" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 2379
  port_range_max    = 2380
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.control_plane.id
}

resource "openstack_networking_secgroup_rule_v2" "cp_kubelet" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 10250
  port_range_max    = 10252
  remote_ip_prefix  = var.private_cidr
  security_group_id = openstack_networking_secgroup_v2.control_plane.id
}

resource "openstack_networking_secgroup_v2" "worker" {
  name        = "k8s-cluster-worker"
  description = "Kubernetes worker nodes"
}

resource "openstack_networking_secgroup_rule_v2" "worker_ssh" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 22
  port_range_max    = 22
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.worker.id
}

resource "openstack_networking_secgroup_rule_v2" "worker_kubelet" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 10250
  port_range_max    = 10250
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.worker.id
}

resource "openstack_networking_secgroup_rule_v2" "worker_nodeport" {
  direction         = "ingress"
  ethertype         = "IPv4"
  protocol          = "tcp"
  port_range_min    = 30000
  port_range_max    = 32767
  remote_ip_prefix  = "0.0.0.0/0"
  security_group_id = openstack_networking_secgroup_v2.worker.id
}


resource "openstack_compute_instance_v2" "control_plane" {
  count = var.control_plane_count

  name        = "k8s-cluster-cp-${count.index + 1}"
  flavor_name = var.cp_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/control-plane.yaml", {
    k8s_version = var.k8s_version
    pod_cidr    = var.pod_cidr
    bootstrap   = count.index == 0
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.os.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 40
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.control_plane[count.index].id
  }
}

resource "openstack_networking_port_v2" "control_plane" {
  count = var.control_plane_count

  name               = "k8s-cluster-cp-${count.index + 1}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.control_plane.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_networking_port_v2" "worker" {
  count = var.worker_count

  name               = "k8s-cluster-worker-${count.index + 1}-port"
  network_id         = openstack_networking_network_v2.private.id
  security_group_ids = [openstack_networking_secgroup_v2.worker.id]

  fixed_ip {
    subnet_id = openstack_networking_subnet_v2.private.id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_compute_instance_v2" "worker" {
  count       = var.worker_count
  name        = "k8s-cluster-worker-${count.index + 1}"
  flavor_name = var.worker_flavor
  key_pair    = var.key_name

  user_data = templatefile("${path.module}/cloud-init/worker.yaml", {
    k8s_version = var.k8s_version
  })

  block_device {
    uuid                  = data.openstack_images_image_v2.os.id
    source_type           = "image"
    destination_type      = "volume"
    volume_size           = 40
    boot_index            = 0
    delete_on_termination = true
  }

  network {
    port = openstack_networking_port_v2.worker[count.index].id
  }

  depends_on = [openstack_networking_router_interface_v2.private]
}

resource "openstack_networking_floatingip_v2" "cp_api" {
  pool = var.external_network
}

resource "openstack_networking_floatingip_associate_v2" "cp_api" {
  floating_ip = openstack_networking_floatingip_v2.cp_api.address
  port_id     = openstack_networking_port_v2.control_plane[0].id
}
variables.tfHCL
variable "control_plane_count" {
  type    = number
  default = 1
}

variable "worker_count" {
  type    = number
  default = 3
}

variable "cp_flavor" {
  type    = string
  default = "m2a.xlarge"
}

variable "worker_flavor" {
  type    = string
  default = "s1a.medium"
}

variable "k8s_version" {
  type    = string
  default = "1.31"
}

variable "pod_cidr" {
  type    = string
  default = "10.244.0.0/16"
}

variable "key_name" {
  description = "Existing SSH keypair name in the project for compute instances"
  type        = string
}

variable "image_name" {
  type    = string
  default = "Ubuntu-24.04"
}

variable "external_network" {
  description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
  type        = string
  default     = "PublicStatic"
}

variable "private_cidr" {
  type    = string
  default = "192.168.70.0/24"
}

variable "dns_nameservers" {
  type    = list(string)
  default = ["8.8.8.8", "8.8.4.4"]
}
outputs.tfHCL
output "api_endpoint" {
  value = "${openstack_networking_floatingip_v2.cp_api.address}:6443"
}

output "control_plane_ips" {
  value = [for i in openstack_compute_instance_v2.control_plane : i.network[0].fixed_ip_v4]
}

output "worker_ips" {
  value = [for i in openstack_compute_instance_v2.worker : i.network[0].fixed_ip_v4]
}
versions.tfHCL
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    openstack = {
      source  = "terraform-provider-openstack/openstack"
      version = "~> 2.0"
    }
  }
}

provider "openstack" {}
terraform.tfvars.exampleHCL
# Required
key_name = "YOUR_KEY_NAME"

# control_plane_count = 1
# worker_count = 3
# cp_flavor = "m2a.xlarge"
# worker_flavor = "s1a.medium"
# k8s_version = "1.31"
# pod_cidr = "10.244.0.0/16"
# image_name = "Ubuntu-24.04"
# external_network = "PublicStatic"
# private_cidr = "192.168.70.0/24"
cloud-init/control-plane.yamlYAML
#cloud-config
package_update: true
package_upgrade: false

write_files:
  - path: /etc/modules-load.d/k8s.conf
    content: |
      overlay
      br_netfilter
  - path: /etc/sysctl.d/k8s.conf
    content: |
      net.bridge.bridge-nf-call-iptables = 1
      net.bridge.bridge-nf-call-ip6tables = 1
      net.ipv4.ip_forward = 1

runcmd:
  - swapoff -a
  - sed -i.bak '/ swap / s/^/#/' /etc/fstab
  - modprobe overlay
  - modprobe br_netfilter
  - sysctl --system
  - apt-get update
  - apt-get install -y apt-transport-https ca-certificates curl gnupg containerd
  - install -m 0755 -d /etc/apt/keyrings
  - curl -fsSL https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
  - echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
  - apt-get update
  - apt-get install -y kubelet kubeadm kubectl conntrack socat
  - apt-mark hold kubelet kubeadm kubectl
  - mkdir -p /etc/containerd
  - containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' > /etc/containerd/config.toml
  - systemctl restart containerd
  - systemctl enable --now containerd
  - systemctl enable --now kubelet
%{ if bootstrap ~}
  - kubeadm init --pod-network-cidr=${pod_cidr}
  - KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
%{ else ~}
  - printf '%s\n' "kubeadm init was not run on this node (non-bootstrap control plane). Obtain control-plane join credentials from the first control plane, then run kubeadm join." > /root/kubeadm-control-plane-join-pending.txt
%{ endif ~}
cloud-init/worker.yamlYAML
#cloud-config
package_update: true
package_upgrade: false

write_files:
  - path: /etc/modules-load.d/k8s.conf
    content: |
      overlay
      br_netfilter
  - path: /etc/sysctl.d/k8s.conf
    content: |
      net.bridge.bridge-nf-call-iptables = 1
      net.bridge.bridge-nf-call-ip6tables = 1
      net.ipv4.ip_forward = 1

runcmd:
  - swapoff -a
  - sed -i.bak '/ swap / s/^/#/' /etc/fstab
  - modprobe overlay
  - modprobe br_netfilter
  - sysctl --system
  - apt-get update
  - apt-get install -y apt-transport-https ca-certificates curl gnupg containerd
  - install -m 0755 -d /etc/apt/keyrings
  - curl -fsSL https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
  - echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
  - apt-get update
  - apt-get install -y kubelet kubeadm conntrack socat
  - apt-mark hold kubelet kubeadm
  - mkdir -p /etc/containerd
  - containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' > /etc/containerd/config.toml
  - systemctl restart containerd
  - systemctl enable --now containerd
  - systemctl enable --now kubelet
  - printf '%s\n' "kubeadm join is not run by cloud-init. On the control plane run kubeadm token create --print-join-command" > /root/kubeadm-join-pending.txt
  - printf '%s\n' "Then SSH to this worker and execute the printed kubeadm join command." >> /root/kubeadm-join-pending.txt
README.mdMarkdown
# Kubernetes Cluster

Kubernetes cluster bootstrap with `kubeadm`: configurable control plane and worker nodes on a private network with external connectivity.


**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.

## Prerequisites

- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- Sufficient quota for control plane and worker flavors and associated volumes (if any)
- Familiarity with `kubeadm` post-bootstrap steps (kubeconfig, CNI, etc.; see project docs)

## Usage

1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`

## Variables

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
| `control_plane_count` | number | no | `1` | Number of control plane nodes |
| `worker_count` | number | no | `3` | Number of worker nodes |
| `cp_flavor` | string | no | `m2a.xlarge` | Flavor for control plane nodes |
| `worker_flavor` | string | no | `s1a.medium` | Flavor for worker nodes |
| `k8s_version` | string | no | `1.31` | Kubernetes version string for bootstrap |
| `pod_cidr` | string | no | `10.244.0.0/16` | Pod network CIDR |
| `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `private_cidr` | string | no | `192.168.70.0/24` | Private network CIDR for nodes |

## Documentation

Full documentation: [Kubernetes cluster template](/docs/automation/templates/k8s-cluster)

Validated variants of this template, each adding one capability the base does not include.

  • Container registryk8s-cluster-registry+$72/mo over the base

    Self-hosted container registry for cluster image pulls

    Show source and download
    9 files. Download the zip or copy any file.Download k8s-cluster-registry.zip
    main.tfHCL
    data "openstack_images_image_v2" "os" {
      name        = var.image_name
      most_recent = true
    }
    
    data "openstack_networking_network_v2" "external" {
      name = var.external_network
    }
    
    resource "openstack_networking_network_v2" "private" {
      name           = "k8s-cluster-private"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "private" {
      name            = "k8s-cluster-private-subnet"
      network_id      = openstack_networking_network_v2.private.id
      cidr            = var.private_cidr
      ip_version      = 4
      enable_dhcp     = true
      dns_nameservers = var.dns_nameservers
    }
    
    resource "openstack_networking_router_v2" "gw" {
      name                = "k8s-cluster-router"
      admin_state_up      = true
      external_network_id = data.openstack_networking_network_v2.external.id
    }
    
    resource "openstack_networking_router_interface_v2" "private" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.private.id
    }
    
    resource "openstack_networking_secgroup_v2" "control_plane" {
      name        = "k8s-cluster-control-plane"
      description = "Kubernetes control plane"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_api" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 6443
      port_range_max    = 6443
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_etcd" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 2379
      port_range_max    = 2380
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_kubelet" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 10250
      port_range_max    = 10252
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_v2" "worker" {
      name        = "k8s-cluster-worker"
      description = "Kubernetes worker nodes"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "worker_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.worker.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "worker_kubelet" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 10250
      port_range_max    = 10250
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.worker.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "worker_nodeport" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 30000
      port_range_max    = 32767
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.worker.id
    }
    
    
    resource "openstack_compute_instance_v2" "control_plane" {
      count = var.control_plane_count
    
      name        = "k8s-cluster-cp-${count.index + 1}"
      flavor_name = var.cp_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/control-plane.yaml", {
        k8s_version = var.k8s_version
        pod_cidr    = var.pod_cidr
        bootstrap   = count.index == 0
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 40
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.control_plane[count.index].id
      }
    }
    
    resource "openstack_networking_port_v2" "control_plane" {
      count = var.control_plane_count
    
      name               = "k8s-cluster-cp-${count.index + 1}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.control_plane.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_port_v2" "worker" {
      count = var.worker_count
    
      name               = "k8s-cluster-worker-${count.index + 1}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.worker.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "worker" {
      count       = var.worker_count
      name        = "k8s-cluster-worker-${count.index + 1}"
      flavor_name = var.worker_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/worker.yaml", {
        k8s_version = var.k8s_version
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 40
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.worker[count.index].id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_floatingip_v2" "cp_api" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "cp_api" {
      floating_ip = openstack_networking_floatingip_v2.cp_api.address
      port_id     = openstack_networking_port_v2.control_plane[0].id
    }
    
    resource "openstack_networking_secgroup_v2" "registry" {
      name        = "k8s-cluster-registry-harbor"
      description = "Container registry HTTPS and SSH"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "registry_https" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 443
      port_range_max    = 443
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.registry.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "registry_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.registry.id
    }
    
    resource "openstack_networking_port_v2" "registry" {
      name               = "k8s-cluster-registry-harbor-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.registry.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "registry" {
      name        = "k8s-cluster-registry-harbor"
      flavor_name = var.registry_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/harbor.yaml", {
        registry_hostname = var.registry_hostname
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 80
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.registry.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.private,
        openstack_compute_instance_v2.control_plane,
      ]
    }
    
    variables.tfHCL
    variable "control_plane_count" {
      type    = number
      default = 1
    }
    
    variable "worker_count" {
      type    = number
      default = 3
    }
    
    variable "cp_flavor" {
      type    = string
      default = "m2a.xlarge"
    }
    
    variable "worker_flavor" {
      type    = string
      default = "s1a.medium"
    }
    
    variable "k8s_version" {
      type    = string
      default = "1.31"
    }
    
    variable "pod_cidr" {
      type    = string
      default = "10.244.0.0/16"
    }
    
    variable "key_name" {
      description = "Existing SSH keypair name in the project for compute instances"
      type        = string
    }
    
    variable "image_name" {
      type    = string
      default = "Ubuntu-24.04"
    }
    
    variable "external_network" {
      description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
      type        = string
      default     = "PublicStatic"
    }
    
    variable "private_cidr" {
      type    = string
      default = "192.168.70.0/24"
    }
    
    variable "dns_nameservers" {
      type    = list(string)
      default = ["8.8.8.8", "8.8.4.4"]
    }
    
    variable "registry_flavor" {
      description = "Flavor for the Harbor registry instance"
      type        = string
      default     = "m2a.large"
    }
    
    variable "registry_hostname" {
      description = "Hostname label for the self-hosted registry (for cloud-init reference)"
      type        = string
      default     = "registry.local"
    }
    
    outputs.tfHCL
    output "api_endpoint" {
      value = "${openstack_networking_floatingip_v2.cp_api.address}:6443"
    }
    
    output "control_plane_ips" {
      value = [for i in openstack_compute_instance_v2.control_plane : i.network[0].fixed_ip_v4]
    }
    
    output "worker_ips" {
      value = [for i in openstack_compute_instance_v2.worker : i.network[0].fixed_ip_v4]
    }
    
    versions.tfHCL
    terraform {
      required_version = ">= 1.6.0"
    
      required_providers {
        openstack = {
          source  = "terraform-provider-openstack/openstack"
          version = "~> 2.0"
        }
      }
    }
    
    provider "openstack" {}
    
    terraform.tfvars.exampleHCL
    # Required
    key_name = "YOUR_KEY_NAME"
    
    # control_plane_count = 1
    # worker_count = 3
    # cp_flavor = "m2a.xlarge"
    # worker_flavor = "s1a.medium"
    # k8s_version = "1.31"
    # pod_cidr = "10.244.0.0/16"
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # private_cidr = "192.168.70.0/24"
    
    cloud-init/control-plane.yamlYAML
    #cloud-config
    package_update: true
    package_upgrade: false
    
    write_files:
      - path: /etc/modules-load.d/k8s.conf
        content: |
          overlay
          br_netfilter
      - path: /etc/sysctl.d/k8s.conf
        content: |
          net.bridge.bridge-nf-call-iptables = 1
          net.bridge.bridge-nf-call-ip6tables = 1
          net.ipv4.ip_forward = 1
    
    runcmd:
      - swapoff -a
      - sed -i.bak '/ swap / s/^/#/' /etc/fstab
      - modprobe overlay
      - modprobe br_netfilter
      - sysctl --system
      - apt-get update
      - apt-get install -y apt-transport-https ca-certificates curl gnupg containerd
      - install -m 0755 -d /etc/apt/keyrings
      - curl -fsSL https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
      - echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
      - apt-get update
      - apt-get install -y kubelet kubeadm kubectl conntrack socat
      - apt-mark hold kubelet kubeadm kubectl
      - mkdir -p /etc/containerd
      - containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' > /etc/containerd/config.toml
      - systemctl restart containerd
      - systemctl enable --now containerd
      - systemctl enable --now kubelet
    %{ if bootstrap ~}
      - kubeadm init --pod-network-cidr=${pod_cidr}
      - KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
    %{ else ~}
      - printf '%s\n' "kubeadm init was not run on this node (non-bootstrap control plane). Obtain control-plane join credentials from the first control plane, then run kubeadm join." > /root/kubeadm-control-plane-join-pending.txt
    %{ endif ~}
    
    cloud-init/harbor.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - ca-certificates
      - curl
    
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        for i in $(seq 1 30); do
          getent hosts registry-1.docker.io >/dev/null 2>&1 && break
          sleep 5
        done
        curl -fsSL https://get.docker.com | sh
        systemctl enable --now docker
        for i in $(seq 1 30); do
          systemctl is-active docker >/dev/null 2>&1 && break
          sleep 5
        done
        mkdir -p /opt/registry/data
        for i in 1 2 3; do
          docker pull registry:2 && break
          sleep 15
        done
        docker rm -f registry >/dev/null 2>&1 || true
        docker run -d --restart=always --name registry \
          -p 443:5000 \
          -v /opt/registry/data:/var/lib/registry \
          registry:2
        for i in $(seq 1 30); do
          nc -z localhost 443 2>/dev/null && touch /var/lib/registry-ready && break
          sleep 10
        done
    
    cloud-init/worker.yamlYAML
    #cloud-config
    package_update: true
    package_upgrade: false
    
    write_files:
      - path: /etc/modules-load.d/k8s.conf
        content: |
          overlay
          br_netfilter
      - path: /etc/sysctl.d/k8s.conf
        content: |
          net.bridge.bridge-nf-call-iptables = 1
          net.bridge.bridge-nf-call-ip6tables = 1
          net.ipv4.ip_forward = 1
    
    runcmd:
      - swapoff -a
      - sed -i.bak '/ swap / s/^/#/' /etc/fstab
      - modprobe overlay
      - modprobe br_netfilter
      - sysctl --system
      - apt-get update
      - apt-get install -y apt-transport-https ca-certificates curl gnupg containerd
      - install -m 0755 -d /etc/apt/keyrings
      - curl -fsSL https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
      - echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
      - apt-get update
      - apt-get install -y kubelet kubeadm conntrack socat
      - apt-mark hold kubelet kubeadm
      - mkdir -p /etc/containerd
      - containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' > /etc/containerd/config.toml
      - systemctl restart containerd
      - systemctl enable --now containerd
      - systemctl enable --now kubelet
      - printf '%s\n' "kubeadm join is not run by cloud-init. On the control plane run kubeadm token create --print-join-command" > /root/kubeadm-join-pending.txt
      - printf '%s\n' "Then SSH to this worker and execute the printed kubeadm join command." >> /root/kubeadm-join-pending.txt
    
    README.mdMarkdown
    # Kubernetes Cluster
    
    Kubernetes cluster bootstrap with `kubeadm`: configurable control plane and worker nodes on a private network with external connectivity.
    
    
    **Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
    
    ## Prerequisites
    
    - OpenTofu >= 1.6.0 or Terraform >= 1.6.0
    - Quake AI account with OpenStack credentials
    - Sufficient quota for control plane and worker flavors and associated volumes (if any)
    - Familiarity with `kubeadm` post-bootstrap steps (kubeconfig, CNI, etc.; see project docs)
    
    ## Usage
    
    1. Clone or copy this template directory
    2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
    3. Source your OpenStack credentials: `source openrc.sh`
    4. Initialize: `tofu init`
    5. Preview: `tofu plan`
    6. Apply: `tofu apply`
    
    ## Variables
    
    | Name | Type | Required | Default | Description |
    | --- | --- | --- | --- | --- |
    | `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
    | `control_plane_count` | number | no | `1` | Number of control plane nodes |
    | `worker_count` | number | no | `3` | Number of worker nodes |
    | `cp_flavor` | string | no | `m2a.xlarge` | Flavor for control plane nodes |
    | `worker_flavor` | string | no | `s1a.medium` | Flavor for worker nodes |
    | `k8s_version` | string | no | `1.31` | Kubernetes version string for bootstrap |
    | `pod_cidr` | string | no | `10.244.0.0/16` | Pod network CIDR |
    | `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `private_cidr` | string | no | `192.168.70.0/24` | Private network CIDR for nodes |
    
    ## Documentation
    
    Full documentation: [Kubernetes cluster template](/docs/automation/templates/k8s-cluster)
    
  • Monitoringk8s-cluster-monitored+$90/mo over the base

    Prometheus and Grafana monitoring for control plane and worker nodes

    Show source and download
    10 files. Download the zip or copy any file.Download k8s-cluster-monitored.zip
    main.tfHCL
    data "openstack_images_image_v2" "os" {
      name        = var.image_name
      most_recent = true
    }
    
    data "openstack_networking_network_v2" "external" {
      name = var.external_network
    }
    
    resource "openstack_networking_network_v2" "private" {
      name           = "k8s-cluster-private"
      admin_state_up = true
    }
    
    resource "openstack_networking_subnet_v2" "private" {
      name            = "k8s-cluster-private-subnet"
      network_id      = openstack_networking_network_v2.private.id
      cidr            = var.private_cidr
      ip_version      = 4
      enable_dhcp     = true
      dns_nameservers = var.dns_nameservers
    }
    
    resource "openstack_networking_router_v2" "gw" {
      name                = "k8s-cluster-router"
      admin_state_up      = true
      external_network_id = data.openstack_networking_network_v2.external.id
    }
    
    resource "openstack_networking_router_interface_v2" "private" {
      router_id = openstack_networking_router_v2.gw.id
      subnet_id = openstack_networking_subnet_v2.private.id
    }
    
    resource "openstack_networking_secgroup_v2" "control_plane" {
      name        = "k8s-cluster-control-plane"
      description = "Kubernetes control plane"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_api" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 6443
      port_range_max    = 6443
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_etcd" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 2379
      port_range_max    = 2380
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_kubelet" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 10250
      port_range_max    = 10252
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_v2" "worker" {
      name        = "k8s-cluster-worker"
      description = "Kubernetes worker nodes"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "worker_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.worker.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "worker_kubelet" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 10250
      port_range_max    = 10250
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.worker.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "worker_nodeport" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 30000
      port_range_max    = 32767
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.worker.id
    }
    
    
    resource "openstack_compute_instance_v2" "control_plane" {
      count = var.control_plane_count
    
      name        = "k8s-cluster-cp-${count.index + 1}"
      flavor_name = var.cp_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/control-plane.yaml", {
        k8s_version = var.k8s_version
        pod_cidr    = var.pod_cidr
        bootstrap   = count.index == 0
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 40
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.control_plane[count.index].id
      }
    }
    
    resource "openstack_networking_port_v2" "control_plane" {
      count = var.control_plane_count
    
      name               = "k8s-cluster-cp-${count.index + 1}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.control_plane.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_port_v2" "worker" {
      count = var.worker_count
    
      name               = "k8s-cluster-worker-${count.index + 1}-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.worker.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "worker" {
      count       = var.worker_count
      name        = "k8s-cluster-worker-${count.index + 1}"
      flavor_name = var.worker_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/worker.yaml", {
        k8s_version = var.k8s_version
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 40
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.worker[count.index].id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_networking_floatingip_v2" "cp_api" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "cp_api" {
      floating_ip = openstack_networking_floatingip_v2.cp_api.address
      port_id     = openstack_networking_port_v2.control_plane[0].id
    }
    
    locals {
      k8s_scrape_targets_yaml = join("\n", concat(
        [for cp in openstack_compute_instance_v2.control_plane : "                - ${yamlencode("${cp.network[0].fixed_ip_v4}:9100")}"],
        [for w in openstack_compute_instance_v2.worker : "                - ${yamlencode("${w.network[0].fixed_ip_v4}:9100")}"]
      ))
    }
    
    resource "openstack_networking_secgroup_rule_v2" "cp_node_exporter" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9100
      port_range_max    = 9100
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.control_plane.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "worker_node_exporter" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9100
      port_range_max    = 9100
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.worker.id
    }
    
    resource "openstack_networking_secgroup_v2" "prometheus" {
      name        = "k8s-cluster-monitored-prometheus"
      description = "Prometheus 9090 from private subnet"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "prometheus_http" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 9090
      port_range_max    = 9090
      remote_ip_prefix  = var.private_cidr
      security_group_id = openstack_networking_secgroup_v2.prometheus.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "prometheus_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.prometheus.id
    }
    
    resource "openstack_networking_secgroup_v2" "grafana" {
      name        = "k8s-cluster-monitored-grafana"
      description = "Grafana 3000 from anywhere"
    }
    
    resource "openstack_networking_secgroup_rule_v2" "grafana_http" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 3000
      port_range_max    = 3000
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.grafana.id
    }
    
    resource "openstack_networking_secgroup_rule_v2" "grafana_ssh" {
      direction         = "ingress"
      ethertype         = "IPv4"
      protocol          = "tcp"
      port_range_min    = 22
      port_range_max    = 22
      remote_ip_prefix  = "0.0.0.0/0"
      security_group_id = openstack_networking_secgroup_v2.grafana.id
    }
    
    resource "openstack_networking_port_v2" "prometheus" {
      name               = "k8s-cluster-monitored-prometheus-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.prometheus.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "prometheus" {
      name        = "k8s-cluster-monitored-prometheus"
      flavor_name = var.prometheus_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/prometheus.yaml", {
        retention_days = var.retention_days
        scrape_targets_yaml = length(local.k8s_scrape_targets_yaml) > 0 ? "        - job_name: \"k8s-nodes\"\n          static_configs:\n            - targets:\n${local.k8s_scrape_targets_yaml}" : ""
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.prometheus.id
      }
    
      depends_on = [
        openstack_networking_router_interface_v2.private,
        openstack_compute_instance_v2.worker,
      ]
    }
    
    resource "openstack_networking_port_v2" "grafana" {
      name               = "k8s-cluster-monitored-grafana-port"
      network_id         = openstack_networking_network_v2.private.id
      security_group_ids = [openstack_networking_secgroup_v2.grafana.id]
    
      fixed_ip {
        subnet_id = openstack_networking_subnet_v2.private.id
      }
    
      depends_on = [openstack_networking_router_interface_v2.private]
    }
    
    resource "openstack_compute_instance_v2" "grafana" {
      name        = "k8s-cluster-monitored-grafana"
      flavor_name = var.grafana_flavor
      key_pair    = var.key_name
    
      user_data = templatefile("${path.module}/cloud-init/grafana.yaml", {
        prometheus_ip        = openstack_compute_instance_v2.prometheus.network[0].fixed_ip_v4
        grafana_admin_password = var.grafana_admin_password
      })
    
      block_device {
        uuid                  = data.openstack_images_image_v2.os.id
        source_type           = "image"
        destination_type      = "volume"
        volume_size           = 20
        boot_index            = 0
        delete_on_termination = true
      }
    
      network {
        port = openstack_networking_port_v2.grafana.id
      }
    
      depends_on = [openstack_compute_instance_v2.prometheus]
    }
    
    resource "openstack_networking_floatingip_v2" "grafana" {
      pool = var.external_network
    }
    
    resource "openstack_networking_floatingip_associate_v2" "grafana" {
      floating_ip = openstack_networking_floatingip_v2.grafana.address
      port_id     = openstack_networking_port_v2.grafana.id
    }
    
    variables.tfHCL
    variable "control_plane_count" {
      type    = number
      default = 1
    }
    
    variable "worker_count" {
      type    = number
      default = 3
    }
    
    variable "cp_flavor" {
      type    = string
      default = "m2a.xlarge"
    }
    
    variable "worker_flavor" {
      type    = string
      default = "s1a.medium"
    }
    
    variable "k8s_version" {
      type    = string
      default = "1.31"
    }
    
    variable "pod_cidr" {
      type    = string
      default = "10.244.0.0/16"
    }
    
    variable "key_name" {
      description = "Existing SSH keypair name in the project for compute instances"
      type        = string
    }
    
    variable "image_name" {
      type    = string
      default = "Ubuntu-24.04"
    }
    
    variable "external_network" {
      description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
      type        = string
      default     = "PublicStatic"
    }
    
    variable "private_cidr" {
      type    = string
      default = "192.168.70.0/24"
    }
    
    variable "dns_nameservers" {
      type    = list(string)
      default = ["8.8.8.8", "8.8.4.4"]
    }
    
    variable "prometheus_flavor" {
      type    = string
      default = "m2a.large"
    }
    
    variable "grafana_flavor" {
      type    = string
      default = "s1a.small"
    }
    
    variable "retention_days" {
      type    = number
      default = 15
    }
    
    variable "grafana_admin_password" {
      type      = string
      sensitive = true
    }
    
    outputs.tfHCL
    output "api_endpoint" {
      value = "${openstack_networking_floatingip_v2.cp_api.address}:6443"
    }
    
    output "control_plane_ips" {
      value = [for i in openstack_compute_instance_v2.control_plane : i.network[0].fixed_ip_v4]
    }
    
    output "worker_ips" {
      value = [for i in openstack_compute_instance_v2.worker : i.network[0].fixed_ip_v4]
    }
    
    
    output "grafana_floating_ip" {
      description = "Public floating IP for Grafana"
      value       = openstack_networking_floatingip_v2.grafana.address
    }
    
    output "prometheus_private_ip" {
      description = "Private IPv4 of the Prometheus instance"
      value       = openstack_compute_instance_v2.prometheus.network[0].fixed_ip_v4
    }
    
    versions.tfHCL
    terraform {
      required_version = ">= 1.6.0"
    
      required_providers {
        openstack = {
          source  = "terraform-provider-openstack/openstack"
          version = "~> 2.0"
        }
      }
    }
    
    provider "openstack" {}
    
    terraform.tfvars.exampleHCL
    # Required
    key_name = "YOUR_KEY_NAME"
    
    # control_plane_count = 1
    # worker_count = 3
    # cp_flavor = "m2a.xlarge"
    # worker_flavor = "s1a.medium"
    # k8s_version = "1.31"
    # pod_cidr = "10.244.0.0/16"
    # image_name = "Ubuntu-24.04"
    # external_network = "PublicStatic"
    # private_cidr = "192.168.70.0/24"
    
    cloud-init/control-plane.yamlYAML
    #cloud-config
    package_update: true
    package_upgrade: false
    
    write_files:
      - path: /etc/modules-load.d/k8s.conf
        content: |
          overlay
          br_netfilter
      - path: /etc/sysctl.d/k8s.conf
        content: |
          net.bridge.bridge-nf-call-iptables = 1
          net.bridge.bridge-nf-call-ip6tables = 1
          net.ipv4.ip_forward = 1
    
    runcmd:
      - swapoff -a
      - sed -i.bak '/ swap / s/^/#/' /etc/fstab
      - modprobe overlay
      - modprobe br_netfilter
      - sysctl --system
      - apt-get update
      - apt-get install -y apt-transport-https ca-certificates curl gnupg containerd
      - install -m 0755 -d /etc/apt/keyrings
      - curl -fsSL https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
      - echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
      - apt-get update
      - apt-get install -y kubelet kubeadm kubectl conntrack socat
      - apt-mark hold kubelet kubeadm kubectl
      - mkdir -p /etc/containerd
      - containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' > /etc/containerd/config.toml
      - systemctl restart containerd
      - systemctl enable --now containerd
      - systemctl enable --now kubelet
    %{ if bootstrap ~}
      - kubeadm init --pod-network-cidr=${pod_cidr}
      - KUBECONFIG=/etc/kubernetes/admin.conf kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml
    %{ else ~}
      - printf '%s\n' "kubeadm init was not run on this node (non-bootstrap control plane). Obtain control-plane join credentials from the first control plane, then run kubeadm join." > /root/kubeadm-control-plane-join-pending.txt
    %{ endif ~}
    
    cloud-init/grafana.yamlYAML
    #cloud-config
    package_update: true
    packages:
      - apt-transport-https
      - ca-certificates
      - curl
      - gnupg
    
    write_files:
      - path: /etc/apt/sources.list.d/grafana.list
        content: deb [signed-by=/usr/share/keyrings/grafana.gpg] https://apt.grafana.com stable main
        owner: root:root
        permissions: "0644"
      - path: /etc/grafana/provisioning/datasources/prometheus.yaml
        content: |
          apiVersion: 1
          datasources:
            - name: Prometheus
              type: prometheus
              access: proxy
              url: http://${prometheus_ip}:9090
              isDefault: true
              editable: false
        owner: root:root
        permissions: "0644"
      - path: /etc/systemd/system/grafana-server.service.d/override.conf
        content: |
          [Service]
          Environment=GF_SECURITY_ADMIN_PASSWORD=${grafana_admin_password}
          Environment=GF_SERVER_HTTP_ADDR=0.0.0.0
        owner: root:root
        permissions: "0600"
    
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        DEV=/dev/sdb
        for i in $(seq 1 12); do [ -b "$DEV" ] && break; sleep 5; done
        if [ -b "$DEV" ]; then
          if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L grafdata "$DEV"; fi
          mkdir -p /var/lib/grafana
          mount "$DEV" /var/lib/grafana
          grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/grafana ext4 defaults,nofail 0 2" >> /etc/fstab
        fi
        PROM="${prometheus_ip}"
        for i in $(seq 1 60); do
          if curl -sf "http://$${PROM}:9090/-/ready" >/dev/null 2>&1; then break; fi
          sleep 5
        done
        install -d /usr/share/keyrings
        curl -fsSL https://apt.grafana.com/gpg.key | gpg --batch --yes --dearmor -o /usr/share/keyrings/grafana.gpg
        for i in 1 2 3; do apt-get update && break; sleep 10; done
        apt-get install -y grafana
        chown -R grafana:grafana /var/lib/grafana
        systemctl daemon-reload
        systemctl enable grafana-server
        systemctl restart grafana-server
    
    cloud-init/prometheus.yamlYAML
    #cloud-config
    package_update: true
    write_files:
      - path: /tmp/prometheus.default
        content: |
          ARGS="--web.enable-lifecycle --web.listen-address=0.0.0.0:9090 --storage.tsdb.retention.time=${retention_days}d"
        owner: root:root
        permissions: "0644"
      - path: /tmp/prometheus.yml
        content: |
          global:
            scrape_interval: 15s
          scrape_configs:
            - job_name: "prometheus"
              static_configs:
                - targets: ["localhost:9090"]
    ${scrape_targets_yaml}
        owner: root:root
        permissions: "0644"
    runcmd:
      - |
        set -e
        export DEBIAN_FRONTEND=noninteractive
        DEV=/dev/sdb
        for i in $(seq 1 12); do [ -b "$DEV" ] && break; sleep 5; done
        if [ -b "$DEV" ]; then
          if ! blkid "$DEV" >/dev/null 2>&1; then mkfs.ext4 -F -L promdata "$DEV"; fi
          mkdir -p /var/lib/prometheus
          mount "$DEV" /var/lib/prometheus
          grep -q "$DEV" /etc/fstab || echo "$DEV /var/lib/prometheus ext4 defaults,nofail 0 2" >> /etc/fstab
        fi
        for i in 1 2 3; do apt-get update && break; sleep 10; done
        apt-get install -y prometheus
        cp /tmp/prometheus.yml /etc/prometheus/prometheus.yml
        cp /tmp/prometheus.default /etc/default/prometheus
        chown -R prometheus:prometheus /var/lib/prometheus
        systemctl enable prometheus
        systemctl restart prometheus
    
    cloud-init/worker.yamlYAML
    #cloud-config
    package_update: true
    package_upgrade: false
    
    write_files:
      - path: /etc/modules-load.d/k8s.conf
        content: |
          overlay
          br_netfilter
      - path: /etc/sysctl.d/k8s.conf
        content: |
          net.bridge.bridge-nf-call-iptables = 1
          net.bridge.bridge-nf-call-ip6tables = 1
          net.ipv4.ip_forward = 1
    
    runcmd:
      - swapoff -a
      - sed -i.bak '/ swap / s/^/#/' /etc/fstab
      - modprobe overlay
      - modprobe br_netfilter
      - sysctl --system
      - apt-get update
      - apt-get install -y apt-transport-https ca-certificates curl gnupg containerd
      - install -m 0755 -d /etc/apt/keyrings
      - curl -fsSL https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/Release.key | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg
      - echo "deb [signed-by=/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v${k8s_version}/deb/ /" > /etc/apt/sources.list.d/kubernetes.list
      - apt-get update
      - apt-get install -y kubelet kubeadm conntrack socat
      - apt-mark hold kubelet kubeadm
      - mkdir -p /etc/containerd
      - containerd config default | sed 's/SystemdCgroup = false/SystemdCgroup = true/' > /etc/containerd/config.toml
      - systemctl restart containerd
      - systemctl enable --now containerd
      - systemctl enable --now kubelet
      - printf '%s\n' "kubeadm join is not run by cloud-init. On the control plane run kubeadm token create --print-join-command" > /root/kubeadm-join-pending.txt
      - printf '%s\n' "Then SSH to this worker and execute the printed kubeadm join command." >> /root/kubeadm-join-pending.txt
    
    README.mdMarkdown
    # Kubernetes Cluster
    
    Kubernetes cluster bootstrap with `kubeadm`: configurable control plane and worker nodes on a private network with external connectivity.
    
    
    **Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
    
    ## Prerequisites
    
    - OpenTofu >= 1.6.0 or Terraform >= 1.6.0
    - Quake AI account with OpenStack credentials
    - Sufficient quota for control plane and worker flavors and associated volumes (if any)
    - Familiarity with `kubeadm` post-bootstrap steps (kubeconfig, CNI, etc.; see project docs)
    
    ## Usage
    
    1. Clone or copy this template directory
    2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values
    3. Source your OpenStack credentials: `source openrc.sh`
    4. Initialize: `tofu init`
    5. Preview: `tofu plan`
    6. Apply: `tofu apply`
    
    ## Variables
    
    | Name | Type | Required | Default | Description |
    | --- | --- | --- | --- | --- |
    | `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
    | `control_plane_count` | number | no | `1` | Number of control plane nodes |
    | `worker_count` | number | no | `3` | Number of worker nodes |
    | `cp_flavor` | string | no | `m2a.xlarge` | Flavor for control plane nodes |
    | `worker_flavor` | string | no | `s1a.medium` | Flavor for worker nodes |
    | `k8s_version` | string | no | `1.31` | Kubernetes version string for bootstrap |
    | `pod_cidr` | string | no | `10.244.0.0/16` | Pod network CIDR |
    | `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
    | `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
    | `private_cidr` | string | no | `192.168.70.0/24` | Private network CIDR for nodes |
    
    ## Documentation
    
    Full documentation: [Kubernetes cluster template](/docs/automation/templates/k8s-cluster)
    
Resources, parameters, and variables
Provisions
Parameterized by
Variables
  • control_plane_count=1
  • worker_count=3
  • cp_flavor="m2a.xlarge"
  • worker_flavor="s1a.medium"
  • k8s_version="1.31"
  • pod_cidr="10.244.0.0/16"
  • key_namerequired
  • image_name="Ubuntu-24.04"
  • external_network="PublicStatic"
  • private_cidr="192.168.70.0/24"
  • dns_nameservers=["8.8.8.8", "8.8.4.4"]

Customize this pattern#

See also#

Usage Guidelines

The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.

For the full policy, see Usage Guidelines.

Last validated: 07.07.2026

Was this page helpful?