Reference
kube.tf Configuration¶
Complete reference for the kube.tf file that defines the KUP6S Kubernetes cluster infrastructure.
Overview¶
The kube.tf file is the main OpenTofu configuration file for the KUP6S cluster. It uses the kube-hetzner community module to provision a K3S cluster on Hetzner Cloud.
Location: kube-hetzner/kube.tf
Purpose:
Define cluster topology (control planes, agent nodes)
Configure infrastructure components (storage, networking)
Specify extra Kubernetes manifests to deploy
Manage cluster-wide settings
Module Source¶
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
# Version managed by kube-hetzner project
}
Uses the official kube-hetzner Terraform/OpenTofu module from the Terraform Registry.
Cluster Identity¶
Cluster Name¶
cluster_name = "kup6s"
Purpose: Identifier for the cluster, used in:
Node naming (
kup6s-control-fsn1-xyz)Network names
Resource tagging
Base Domain¶
base_domain = "cluster.kup6s.com"
Purpose: Base domain for reverse DNS entries in Hetzner Cloud. Node FQDNs take the form nodename.cluster.kup6s.com.
Control Plane Node Pools¶
The KUP6S cluster runs 3 control plane nodes across 3 data centers for high availability (embedded etcd, 3-member quorum). Per list-index immutability, retired pools are kept as count = 0 — never reordered or removed.
control_plane_nodepools = [
{
name = "control-fsn1"
server_type = "cax21" # ARM64
location = "fsn1"
count = 1
swap_size = "2G"
longhorn_volume_size = 0
},
{
name = "control-nbg1"
server_type = "cax31" # inactive — replaced by control-nbg1-2 (cx33)
location = "nbg1"
count = 0
},
{
name = "control-hel1"
server_type = "cax21" # never provisioned
location = "hel1"
count = 0
},
{
name = "control-hel1-2"
server_type = "cx33" # x86
location = "hel1"
count = 1
swap_size = "2G"
longhorn_volume_size = 0
},
{
name = "control-nbg1-2"
server_type = "cx33" # x86 — replaces the retired cax31 control-nbg1
location = "nbg1"
count = 1
swap_size = "2G"
longhorn_volume_size = 0
},
]
Active Control Planes¶
Node |
Type |
Arch |
vCPU |
RAM |
Location |
|---|---|---|---|---|---|
control-fsn1 |
cax21 |
ARM64 |
4 |
8 GB |
fsn1 |
control-hel1-2 |
cx33 |
x86 |
4 |
8 GB |
hel1 |
control-nbg1-2 |
cx33 |
x86 |
4 |
8 GB |
nbg1 |
Rationale:
Multi-DC (fsn1/hel1/nbg1): survives a data-center failure; embedded etcd keeps a 3-member quorum.
Mixed arch: fsn1 stays on cheap ARM
cax21; nbg1 + hel1 use x86cx33— better Hetzner availability (ARMcaxis often sold out in nbg1) at a similar price.Control planes run no Longhorn (
longhorn_volume_size = 0).
Agent Node Pools¶
The worker tier is hybrid. The heavy lifting runs on 2× dedicated EX63 bare-metal servers (Hetzner Robot, Helsinki), joined as external K3S agents — these are not part of agent_nodepools (Robot servers are provisioned and joined separately; see Add a dedicated Robot worker). The only remaining cloud agent is a single ARM node reserved for ARM CI builds.
agent_nodepools = [
{
name = "agent-cax31-fsn1" # "yim" - dedicated ARM CI builder
server_type = "cax31" # ARM64, 8 vCPU / 16 GB
location = "fsn1"
taints = ["dedicated=arm-ci:NoSchedule"] # only gitlab-runner-arm tolerates it
count = 1
swap_size = "2G"
longhorn_volume_size = 0
},
# --- all other cloud agents inactive (count = 0; index preserved) ---
{ name = "agent-cpx22-fsn1", server_type = "cpx22", location = "fsn1", count = 0,
taints = ["kubernetes.io/arch=amd64:NoSchedule"] },
{ name = "agent-cax31-nbg1", server_type = "cax31", location = "nbg1", count = 0 },
{ name = "agent-cx43-fsn1", server_type = "cx43", location = "fsn1", count = 0 },
{ name = "agent-cx43-nbg1", server_type = "cx43", location = "nbg1", count = 0 },
{ name = "agent-cx43-hel1", server_type = "cx43", location = "hel1", count = 0 },
]
Worker Capacity¶
Worker |
Provisioning |
Arch |
vCPU |
RAM |
Storage |
Location |
|---|---|---|---|---|---|---|
kup6s-ex-hel-1 |
Robot (EX63) |
x86 |
20 cores |
64 GB |
2× 1 TB NVMe |
hel (ex-hel-a) |
kup6s-ex-hel-2 |
Robot (EX63) |
x86 |
20 cores |
64 GB |
2× 1 TB NVMe |
hel (ex-hel-b) |
agent-cax31-fsn1 (“yim”) |
Cloud (cax31) |
ARM64 |
8 |
16 GB |
— (no Longhorn) |
fsn1 |
Strategy:
EX63 dedicated workers carry all general workloads and Longhorn storage (~2 TB usable, 2-way replication with hard anti-affinity across the two hosts). Joined over vSwitch 82213 (private subnet
10.6.0.0/16); LB targets + node lifecycle are managed by the Robot-enabled hcloud CCM.yim is tainted
dedicated=arm-ci:NoScheduleand runs onlygitlab-runner-arm— it is not a general scheduling target.The
count = 0entries are inactive pools kept only to preserve list indices — kube-hetzner tracks pools by position, so they must never be reordered or removed.
Key Configuration Variables¶
Security¶
WireGuard Encryption¶
enable_wireguard = true
Effect: Encrypts all pod-to-pod communication across nodes using WireGuard.
Rationale: Security requirement for sensitive data in transit.
Storage¶
Longhorn Distributed Storage¶
enable_longhorn = true
Effect: Deploys Longhorn distributed block storage for persistent volumes.
Components:
Longhorn manager on each node
CSI driver for volume provisioning
Backup functionality (CIFS)
Hetzner CSI Driver¶
disable_hetzner_csi = true
Status: Disabled.
Effect: The Hetzner Cloud CSI driver and the hcloud-volumes StorageClass are not deployed, and there are no hcloud Cloud Volumes. All persistent storage is Longhorn on the dedicated EX63 NVMe disks.
Note: The SMB CSI driver (Hetzner Storage Box, CIFS — the Longhorn backup target) and the hcloud CCM remain. Don’t confuse the CCM with the CSI driver.
Scheduling¶
Control Plane Scheduling¶
# allow_scheduling_on_control_plane = true
Status: Commented (disabled by default)
Effect: When enabled, allows non-system pods to schedule on control plane nodes.
Current: Control planes run only system components.
Credential Management¶
CRITICAL: All sensitive values are provided via environment variables, not hardcoded in kube.tf.
Required Environment Variables¶
Variable |
Purpose |
|---|---|
|
Hetzner Cloud API access |
|
Hetzner Object Storage access |
|
Hetzner Object Storage secret |
|
Longhorn backup target URL |
|
Backup storage username |
|
Backup storage password |
|
Traefik dashboard user |
|
Traefik dashboard password |
Loading Credentials¶
# Create .env file from template
cp .env.example .env
# Edit .env with your credentials
# Load credentials (bash)
set -a
source .env
set +a
# Or use fish shell with dotenv plugin
fish -c "dotenv .env; and tofu plan"
Variable Declaration¶
In kube.tf:
variable "hcloud_token" {
description = "Hetzner Cloud API token"
type = string
sensitive = true
}
provider "hcloud" {
token = var.hcloud_token # From TF_VAR_hcloud_token
}
Anti-Pattern (NEVER):
# ❌ WRONG - hardcoded secret
provider "hcloud" {
token = "abc123secrettoken"
}
Extra Manifests¶
extra_manifests = ["kube-hetzner/extra-manifests/kustomization.yaml"]
Purpose: Deploys additional Kubernetes manifests via Kustomize during cluster creation.
Location: kube-hetzner/extra-manifests/
Contents: Infrastructure-tier components (ArgoCD, Crossplane, ESO, CNPG, monitoring)
See: Extra Manifests
Node Pool Parameters Reference¶
Common Parameters¶
Parameter |
Type |
Description |
|---|---|---|
|
string |
Unique identifier for the node pool |
|
string |
Hetzner server type (cax21, cpx31, etc.) |
|
string |
Hetzner data center (fsn1, nbg1, hel1) |
|
list(string) |
Kubernetes labels applied to nodes |
|
list(string) |
Kubernetes taints (e.g., |
|
number |
Number of nodes in this pool |
|
string |
Swap space size (e.g., “2G”) |
|
number |
Longhorn storage reservation (0 = disabled) |
Available Server Types¶
ARM64 (CAX series):
cax11: 2 vCPU, 4GB RAM, 40GB SSD - €3.79/monthcax21: 4 vCPU, 8GB RAM, 80GB SSD - €7.99/month (control-fsn1)cax31: 8 vCPU, 16GB RAM, 160GB SSD - €15.99/month (agent “yim”)
AMD64 (CX series, Intel):
cx33: 4 vCPU, 8GB RAM - €6.49/month (control-hel1-2, control-nbg1-2)cx43: 8 vCPU, 16GB RAM (retired agent type)
Prices are net/month and change over time — confirm current rates in the Hetzner console.
Dedicated (Robot): the EX63 bare-metal workers are not cloud server_types — they are ordered/managed via Hetzner Robot and joined separately (see the dedicated-worker how-to), not through agent_nodepools.
Locations:
fsn1: Falkenstein, Germanynbg1: Nuremberg, Germanyhel1: Helsinki, Finland (dedicated EX63 workers live here)
Modifying the Configuration¶
Adding a Node Pool¶
Edit
kube.tf:agent_nodepools = [ # ...existing pools... { name = "agent-arm-4" server_type = "cax41" location = "fsn1" labels = ["workload=heavy"] taints = [] count = 2 swap_size = "4G" longhorn_volume_size = 0 } ]
Preview changes:
cd kube-hetzner source .env tofu plan
Apply:
tofu apply
Changing Node Count¶
Scaling a pool up or down:
{
name = "agent-arm-2"
count = 5 # Changed from 3 to 5
# ...other parameters...
}
Note: Scaling down removes nodes. Ensure workloads are drained first.
Modifying Taints¶
Remove AMD64 taint to allow multi-arch scheduling:
{
name = "agent-amd-3"
taints = [] # Removed: ["kubernetes.io/arch=amd64:NoSchedule"]
# ...
}
Safety Guidelines¶
Before Modifying kube.tf¶
Backup: Ensure recent cluster backup exists
Review: Understand impact of changes
Plan: Always run
tofu planfirstTest: Test in dev/staging if possible
Monitor: Watch cluster during apply
Dangerous Operations¶
Changing Control Plane Configuration:
Can cause control plane downtime
May require cluster recreation
Test in staging first
Removing Agent Nodes:
Ensure no critical workloads on nodes
Drain nodes before scaling down:
kubectl drain <node-name> --ignore-daemonsets --delete-emptydir-data
Changing Storage Settings:
May cause Longhorn data loss
Backup all PVs first
Further Reading¶
Explanation: Infrastructure as Code - IaC philosophy
How-To: Apply Infrastructure Changes - Safe modification procedures
Tutorial: Deploy Your First Cluster - Hands-on cluster creation
Reference: Extra Manifests - Infrastructure components