Kubernetes Cluster Bootstrap
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#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist in your project) | required |
control_plane_count | Number of control plane nodes | 1 |
worker_count | Number of worker nodes | 3 |
cp_flavor | Control plane instance size | m2a.xlarge |
worker_flavor | Worker node instance size | s1a.medium |
k8s_version | Kubernetes version | 1.31 |
pod_cidr | Pod network CIDR | 10.244.0.0/16 |
image_name | Operating system image | Ubuntu-24.04 |
external_network | External network for router gateway and floating IP | PublicStatic |
private_cidr | Address range for the cluster subnet | 192.168.70.0/24 |
dns_nameservers | Resolvers 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.
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
3× Worker node
s1a.medium · 4 shared vCPU, 4 GiB RAM, 0.5 Gbps
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
s1a.medium
4 shared vCPU, 4 GiB RAM, 0.5 Gbps
s1a.medium
4 shared vCPU, 4 GiB RAM, 0.5 Gbps
s1a.medium
4 shared vCPU, 4 GiB RAM, 0.5 Gbps
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
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
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 morePrivate 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.
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.
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 moreDev/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.
Production on the configured CPU
The headline estimate above; predictable steady-load performance.
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#
Show source (8 files)Hide source
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
}
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"]
}
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]
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# 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-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-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
# 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)
Variations
Deploy walkthroughValidated variants of this template, each adding one capability the base does not include.
- Container registry
k8s-cluster-registry+$72/mo over the baseSelf-hosted container registry for cluster image pulls
Show source and downloadHide source
9 files. Download the zip or copy any file.Download k8s-cluster-registry.zipmain.tfHCLdata "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.tfHCLvariable "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.tfHCLoutput "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.tfHCLterraform { 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 donecloud-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.txtREADME.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) - Monitoring
k8s-cluster-monitored+$90/mo over the basePrometheus and Grafana monitoring for control plane and worker nodes
Show source and downloadHide source
10 files. Download the zip or copy any file.Download k8s-cluster-monitored.zipmain.tfHCLdata "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.tfHCLvariable "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.tfHCLoutput "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.tfHCLterraform { 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-servercloud-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 prometheuscloud-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.txtREADME.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
control_plane_count=1worker_count=3cp_flavor="m2a.xlarge"worker_flavor="s1a.medium"k8s_version="1.31"pod_cidr="10.244.0.0/16"key_namerequiredimage_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#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
- Deploy the Kubernetes cluster bootstrap template with OpenTofu: end-to-end tutorial that applies this template, joins a worker with kubeadm, runs a test workload, and tears the stack down
- Kubernetes service overview
- Private Network + VPN template
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