kh-assistant
mysticaltech/terraform-hcloud-kube-hetzner/.claude/skills/kh-assistant/SKILL.md
Use when users need help with kube-hetzner configuration, debugging, or questions - acts as an intelligent assistant with live repo access
Skill3.9k starsChanged 29 days ago
- Reads credentials
What's in it
- KH Assistant
- Startup Checklist
- Route to Sibling Skills First
- Knowledge Sources
- Primary Documentation Files
- Specialized Documentation
- GitHub (Live Data)
- Current v3 Baseline
- Critical Rules
- MUST Follow — Never Violate
- Common Mistakes to Prevent
- v3 Topology Shortcuts
- Common Issues Catalog
- v3.0.0 Regressions Fixed in v3.0.1
- Known Error Patterns
- Debugging Workflow
- Hetzner Cloud Context
- Server Types (x86)
- Server Types (ARM — CAX, cost-optimized)
- Locations
- Configuration Workflows
- Workflow: Creating kube.tf
- Workflow: Debugging
- Workflow: Teardown / Destroy
- Workflow: Feature Questions
- Workflow: Upgrades
- Workflow: v2 -> v3 Migrations
- Configuration Templates
- Minimal Development (Single Node)
- Production HA (3 Control Planes + Workers)
---
name: kh-assistant
description: Use when users need help with kube-hetzner configuration, debugging, or questions - acts as an intelligent assistant with live repo access
---
# KH Assistant
Expert assistant for **terraform-hcloud-kube-hetzner** — deploying production-ready k3s/RKE2 clusters on Hetzner Cloud.
## Startup Checklist
**ALWAYS do these first before answering any question:**
```bash
# 1. Get latest release version
gh release list --repo kube-hetzner/terraform-hcloud-kube-hetzner --limit 1 --json tagName,publishedAt
# 2. Read key files for context; use exact search to scope large files
# - variables.tf — all configurable options
# - docs/llms.md — PRIMARY comprehensive documentation (~60k tokens)
# - kube.tf.example — working example
# - CHANGELOG.md — recent changes
```
**For Hetzner-specific info** (server types, pricing, locations):
```bash
# Use web search
WebSearch "hetzner cloud server types pricing 2026"
```
---
## Route to Sibling Skills First
**Do not hand-solve a specialized workflow inline when a sibling skill matches.**
Recommend the skill, explain why it fits, and invoke it when available. End users
should mainly be routed to `migrate-v2-to-v3`, `upgrade-cluster`, and
`debug-node`; maintainer-only skills are for repository operations, not normal
cluster support.
| User intent | Skill | What it does | Invocation |
|-------------|-------|--------------|------------|
| Migrate an existing Terraform root or cluster from module v2.x to v3.x | `migrate-v2-to-v3` | Audits and rewrites the v2 contract, preserves state, and enforces the protected-infrastructure plan gate | `/migrate-v2-to-v3` |
| Upgrade or harden a live cluster, module/providers, k3s/RKE2, or replace nodes safely | `upgrade-cluster` | Separates module convergence from runtime rollout and proves Terraform plus Kubernetes health | `/upgrade-cluster` |
| Diagnose an unreachable node, SSH/cloud-init failure, or stuck provisioning | `debug-node` | Uses Hetzner rescue mode to mount and inspect the node without working node SSH | `/debug-node` |
| Validate module changes with Terraform and OpenTofu | `test-changes` | Runs formatting, validation, compatibility, example, and plan gates against a supplied test root | `/test-changes` |
| Implement a GitHub issue (**maintainer only**) | `fix-issue` | Fetches and verifies the issue, implements the root-cause fix, tests it, and preserves contributor credit | `/fix-issue <number>` |
| Classify and respond to a GitHub issue (**maintainer only**) | `triage-issue` | Checks evidence and duplicates, classifies the report, and drafts the appropriate response/action | `/triage-issue <number>` |
| Review a pull request (**maintainer only**) | `review-pr` | Performs a security, compatibility, regression, and code-quality review of an untrusted contribution | `/review-pr <number>` |
| Synchronize project documentation (**maintainer only**) | `sync-docs` | Keeps `variables.tf`, generated/reference docs, examples, migration docs, and skills coherent | `/sync-docs` |
| Prepare or execute a release (**maintainer only**) | `prepare-release` | Verifies release content and versions; tags/pushes only with explicit maintainer release authority | `/prepare-release` |
| Prove risky changes across the live v3 matrix (**maintainer only**) | `running-stabilization-loop` | Iteratively runs, diagnoses, fixes, and reruns the k3s/RKE2 matrix plus tagged-version upgrade paths | `/running-stabilization-loop` |
If the matching skill is not installed, tell the user to install from the
project repository and then invoke it:
```bash
# Interactive selection
npx skills add kube-hetzner/terraform-hcloud-kube-hetzner
# Install only the recommended skill (example)
npx skills add kube-hetzner/terraform-hcloud-kube-hetzner --skill migrate-v2-to-v3
# Install globally for supported agents
npx skills add kube-hetzner/terraform-hcloud-kube-hetzner -g
```
Do not recommend maintainer-only skills to end users unless they are explicitly
contributing to or maintaining this repository.
---
## Knowledge Sources
### Primary Documentation Files
| File | Purpose | When to Use |
|------|---------|-------------|
| `docs/llms.md` | **PRIMARY** - Comprehensive variable reference | First stop for any variable question |
| `variables.tf` | Variable definitions with types/defaults | Verify exact syntax and defaults |
| `locals.tf` | Core logic and computed values | Understanding how features work |
| `kube.tf.example` | Complete working example | Template for configurations |
| `CHANGELOG.md` | Version history, breaking changes | Upgrade questions, "when was X added" |
| `MIGRATION.md` | Canonical old-to-new migration variable map | v2 -> v3 upgrade questions |
| `docs/v2-to-v3-migration.md` | v2 -> v3 operator playbook | Existing-cluster major upgrades |
| `docs/v3-release-evidence.md` | Live v3 proof, CI caveats, RKE2 sizing evidence | Release readiness, "is this proven?" questions |
| `docs/v3-topology-recommendations.md` | v3 topology chooser and "what not to choose" rules | New designs, multinetwork, Gateway API, registry mirror |
| `docs/selinux.md` | SELinux policy provenance and AVC workflow | Workload denials, policy proposals, disable-vs-fix decisions |
| `README.md` | Project overview, quick start | New user orientation |
### Specialized Documentation
| File | Topic |
|------|-------|
| `docs/terraform.md` | Auto-generated terraform docs |
| `docs/ssh.md` | SSH configuration, key formats |
| `docs/add-robot-server.md` | Hetzner dedicated server integration |
| `docs/private-network-egress.md` | NAT router setup for private clusters |
| `docs/customize-mount-path-longhorn.md` | Longhorn storage customization |
### GitHub (Live Data)
```bash
# Latest release
gh release list --repo kube-hetzner/terraform-hcloud-kube-hetzner --limit 1
# Search issues for errors
gh issue list --repo kube-hetzner/terraform-hcloud-kube-hetzner --search "<error>" --state all
# Search discussions for how-to
gh api repos/kube-hetzner/terraform-hcloud-kube-hetzner/discussions --jq '.[].title'
# Check if variable exists
grep 'variable "<name>"' variables.tf
```
### Current v3 Baseline
Verify the live tag at startup; the checked-in release baseline is **v3.2.1**.
| Fact | Current contract |
|------|------------------|
| Kubernetes distribution | k3s is the default; RKE2 is supported via `kubernetes_distribution = "rke2"` |
| Kubernetes version policy | k3s defaults to the upstream `stable` channel; pin a version/channel when reproducibility or v2 minor preservation matters |
| Node OS | Brand-new nodepools default to Leap Micro; existing MicroOS nodepools remain supported and are preserved on normal v2 upgrades |
| Addon versions | Unset addon version inputs use the reviewed deterministic module matrix; `latest` is an explicit opt-in to floating upstream behavior |
---
## Critical Rules
### MUST Follow — Never Violate
| Rule | Explanation |
|------|-------------|
| **At least 1 control plane** | `control_plane_nodepools` must have at least one entry with `count >= 1` |
| **Supported OS only** | New nodes default to Leap Micro; MicroOS is legacy/upgrade support. Never suggest Ubuntu, Debian, or other generic OS images. |
| **Network region coverage** | `network_region` must contain ALL node locations |
| **Odd control plane counts for HA** | Use 1, 3, or 5 — never 2 or 4 (quorum requirement) |
| **Autoscaler is separate** | `autoscaler_nodepools` is independent from `agent_nodepools` |
| **Latest version always** | Always fetch and use the latest release tag |
### Common Mistakes to Prevent
| Mistake | Correct |
|---------|---------|
| Empty control_plane_nodepools | At least one with count >= 1 |
| 2 control planes for "HA" | Use 3 (odd number for quorum) |
| Suggesting Ubuntu/Debian | Use Leap Micro by default; MicroOS only for legacy/explicit nodepools |
| Location not in network_region | network_region must cover all locations |
| Confusing autoscaler with agents | Autoscaler pools are completely separate |
| Using old version | Always check latest release first |
| Using v2 input names in v3 | Rewrite with `MIGRATION.md`: `enable_*` booleans, `kubernetes_distribution`, `k3s_channel`, `rke2_channel`, `node_transport_mode`, and `network_subnet_mode` |
| Raw Hetzner private multinetwork for >100 nodes | Use `node_transport_mode = "tailscale"` or the experimental Cilium public overlay; Hetzner private Networks do not route to each other |
| Treating external Tailscale hooks as node transport | Use `node_transport_mode = "tailscale"` for cluster transport; use `node_connection_overrides` only for user-owned operator access |
| Treating Cloudflare Mesh/WARP as supported node transport | Use Tailscale for kube-hetzner-managed secure node transport; Cloudflare Access/Tunnel is external operator/app access only |
| Assuming one Hetzner Network can exceed 100 nodes | Shard across multiple Hetzner Networks and count all attachments, including control planes, static agents, autoscaler `max_nodes`, NAT routers, and load balancers |
| Promising static 10k placement spread in one project | Hetzner spread groups are 10 servers each and 50 groups per project; use autoscaler/network shards or split across projects/clusters |
| Confusing Cilium Gateway API with Traefik Gateway provider | Use `cilium_gateway_api_enabled` for Cilium, `traefik_provider_kubernetes_gateway_enabled` for Traefik |
| Enabling Cilium Gateway API with kube-proxy | Requires `cni_plugin = "cilium"` and `enable_kube_proxy = false` |
| Enabling embedded registry mirror on low-trust nodes | Use only for equal-trust clusters; warn about credential sharing and tag poisoning |
| Disabling SELinux globally for one workload denial | Follow `docs/selinux.md`: collect AVCs, try udica, use per-pool `selinux = false` only as the last resort |
| Assuming RKE2 needs 8GB control planes | v3 size-aware kubelet reservations make 4GB `cx23` control planes viable; still size production for workload headroom |
| Manual cloud deletes during teardown | Use `scripts/destroy.sh` first; `scripts/cleanup.sh` is the forceful fallback and targets the token's entire HCloud project |
### v3 Topology Shortcuts
| Need | Recommendation |
|------|----------------|
| Small dev | Single control plane, one small agent pool, no ingress unless needed |
| Normal HA | 3 control planes, 2+ agents, one primary Hetzner Network |
| Private-only | NAT router and private control-plane LB on the primary Network |
| Secure API/SSH | `node_transport_mode = "tailscale"` and close public API/SSH firewall sources |
| Cloudflare-protected operator/app access | User-managed Cloudflare Access/Tunnel in front of kube API, SSH, Rancher, Grafana, or ingress; keep node transport as Hetzner private or Tailscale |
| +100 Cloud nodes | Tailscale node transport plus one external Hetzner Network shard per 100-node budget |
| 10k reference | Autoscaler-first Tailscale multinetwork; point to `examples/tailscale-node-transport/massive-10000-nodes.tf.example` |
| Cilium Gateway API | Cilium, `enable_kube_proxy = false`, `cilium_gateway_api_enabled = true` |
| Heavy image pulls | `embedded_registry_mirror.enabled = true` only on trusted clusters |
---
## Common Issues Catalog
### v3.0.0 Regressions Fixed in v3.0.1
- **Zero-agent post-apply plan failure (#2236, #2238):** v3.0.0 clusters
with `agent_nodepools = []` failed later plans with
`no change found for terraform_data.agents`. Upgrade to v3.0.1 or later; it routes
readiness triggers through a single agent-id aggregator, without replacing
or rerunning post-install readiness during the upgrade.
- **Static agents assigned to the wrong subnet (#2239):** v3.0.0 assigned
primary-network static agents from the control-plane subnet. v3.0.1 or later restores
per-nodepool addressing. Clusters first created on v3.0.0 require the release
note's rolling agent migration: cordon/drain one agent, target-apply that
agent's in-place private-NIC detach/reattach and k3s/RKE2 config restart,
verify it is `Ready` on the intended subnet, uncordon it, and continue. If
interface mapping does not recover, reboot so `kh-rename-interface.service`
can verify it. Normal clusters upgraded from v2.x retain the v2 IP formula
and are unaffected.
### Known Error Patterns
| Error | Cause | Solution |
|-------|-------|----------|
| `cannot sum empty list` | control_plane_nodepools is empty or all counts are 0 | Add at least one control plane with count >= 1 |
| `NAT router primary IPs will be replaced` | Pre-v2.19.0 used deprecated 'datacenter' attribute | Allow recreation (IPs change) or do state migration |
| `Traefik returns 404 for all routes` | Traefik v34+ config change | Upgrade to module v2.19.0+ |
| `SSH connection refused or timeout` | Key format, firewall, or node not ready | Check ssh_public_key format, verify firewall_ssh_source |
| `Node stuck in NotReady` | Network region mismatch or token issues | Ensure network_region contains all node locations |
| `Error creating network subnet` | Subnet CIDR conflicts | Check network_ipv4_cidr doesn't overlap with existing |
| `cloud-init failed` | Leap Micro/MicroOS snapshot missing, wrong region, wrong architecture, or wrong distro label | Recreate snapshots with packer in the correct region/architecture and k3s/RKE2 SELinux variant |
| `resource_already_detaching` or LB network 422 during destroy | Known benign ingress-LB detach race between CCM and Terraform ownership | Run `scripts/destroy.sh`; it retries only this race and then prints an orphan report |
| Network/subnet destroy hangs with autoscaler enabled | Autoscaler-created servers are not in Terraform state and still pin the network | Wait until the control plane is dead, or scale autoscaler `min_nodes = 0`, then delete the orphan |
| SELinux `avc: denied` workload failures | Missing workload policy, not automatically a module bug | Follow `docs/selinux.md`; collect AVC evidence and try udica before upstreaming policy or disabling a pool |
### Debugging Workflow
```
1. Check Common Issues table above
2. Search GitHub issues: gh issue list --search "<error>" --state all
3. Search docs/llms.md for related variables
4. Check locals.tf for the logic
5. Provide: Root cause → Fix → Prevention
6. Link to relevant GitHub issues if found
```
---
## Hetzner Cloud Context
### Server Types (x86)
| Type | vCPU | RAM | Disk | Best For |
|------|------|-----|------|----------|
| `cx23` | 2 | 4GB | 40GB | Minimal dev, small k3s/RKE2 control planes |
| `cx33` | 4 | 8GB | 80GB | Production control plane, moderate workers |
| `cx43` | 8 | 16GB | 160GB | Production workers |
| `cx53` | 16 | 32GB | 320GB | Heavy workloads |
`cx23` is the current minimum used throughout the v3 examples. The RKE2 + Leap
Micro `cx23` control-plane shape is live-proven in `docs/v3-release-evidence.md`
after size-aware kubelet reservations landed.
### Server Types (ARM — CAX, cost-optimized)
| Type | vCPU | RAM | Disk | Best For |
|------|------|-----|------|----------|
| `cax11` | 2 | 4GB | 40GB | ARM dev |
| `cax21` | 4 | 8GB | 80GB | ARM workloads |
| `cax31` | 8 | 16GB | 160GB | ARM production |
| `cax41` | 16 | 32GB | 320GB | ARM heavy |
### Locations
| Region | Locations | Network Zone |
|--------|-----------|--------------|
| Germany | `fsn1`, `nbg1` | `eu-central` |
| Finland | `hel1` | `eu-central` |
| USA East | `ash` | `us-east` |
| USA West | `hil` | `us-west` |
| Singapore | `sin` | `ap-southeast` |
**Rule**: All locations must be in the same `network_region`.
---
## Configuration Workflows
### Workflow: Creating kube.tf
```
1. FIRST: Get latest release
gh release list --repo kube-hetzner/terraform-hcloud-kube-hetzner --limit 1
2. Ask clarifying questions:
- Use case: Production / Development / Testing?
- Kubernetes distribution: k3s (default) / RKE2?
- HA: Single node / 3 control planes / Super-HA (multi-location)?
- Budget: Which server types?
- Network: Public / Private with NAT router?
- CNI: Flannel (default) / Cilium / Calico?
- Storage: Longhorn needed?
- Ingress: Traefik (default) / Nginx / HAProxy?
3. Query variables.tf and docs/llms.md for relevant options
4. Generate complete config with:
- Module source and version (latest!)
- Required: hcloud_token, SSH keys, `kubernetes_distribution` only when not default k3s
- Requested features
- Helpful comments
5. Validate syntax:
terraform fmt -recursive
terraform validate
```
### Workflow: Debugging
```
1. Parse the error:
- Terraform error vs k3s error vs provider error
- Which resource?
- What operation?
2. Check Common Issues Catalog (above)
3. Search GitHub:
gh issue list --search "<error keyword>" --state all
4. Read relevant code:
- locals.tf for logic
- variables.tf for options
- Specific .tf files based on error
5. Provide solution:
- Root cause explanation
- Fix (config change or upgrade)
- Prevention steps
- Link to related issues
```
For K3s certificate expiry, remind the operator that the admin kubeconfig also
contains client certificates. After restarting or rotating control-plane
certificates, retrieve a fresh `/etc/rancher/k3s/k3s.yaml` over SSH from a
healthy control-plane node, save it with mode `0600`, and replace its
`https://127.0.0.1:6443` server with the cluster's reachable API endpoint before
testing it with `kubectl`. Do not tell operators to keep using a stale local
kubeconfig after certificate recovery.
### Workflow: Teardown / Destroy
```
1. Run from the user's Terraform root:
<module-checkout>/scripts/destroy.sh -auto-approve
2. Let the wrapper detect the initialized Terraform/OpenTofu engine. It retries
only recognized benign LB-detach/network-in-use convergence races, then
always prints a read-only hcloud orphan report when credentials and the
cluster name are available.
3. If autoscaler-created servers pin network/subnet deletion, either set every
autoscaler pool `min_nodes = 0` and apply before destroy, or wait until the
control plane is dead. Then use the orphan report to identify and delete only
the autoscaler-created servers, and rerun `scripts/destroy.sh`.
4. Use <module-checkout>/scripts/cleanup.sh only when state is already broken or
the read-only report identifies leftovers. It treats the token's entire
HCloud project as cluster-dedicated, so review its dry run before deletion
and include persistent data only deliberately.
```
### Workflow: Feature Questions
```
1. Check docs/llms.md FIRST (primary reference)
2. Verify in variables.tf (exact syntax)
3. Check kube.tf.example for usage
4. Search GitHub discussions for examples
5. Provide answer with file references
```
### Workflow: Upgrades
```
1. Get current and target versions
2. If this is v2.x -> v3.x, use the /migrate-v2-to-v3 skill workflow
3. Read CHANGELOG.md, MIGRATION.md, and docs/v2-to-v3-migration.md
4. Check for:
- Removed/renamed variables
- Changed defaults
- Required migrations
- Inverted boolean semantics
- State migration requirements
- Network/subnet/LB/server replacement risk
- Production no-destroy gate from `MIGRATION.md`
5. Generate upgrade steps:
- Update version in kube.tf
- terraform init -upgrade
- terraform validate
- terraform plan -out=<planfile> (check for destructions!)
- terraform show -json <planfile> and run the protected hcloud no-destroy gate from `MIGRATION.md`
- terraform apply
6. Warn if terraform plan shows resource recreation
```
### Workflow: v2 -> v3 Migrations
Use `.claude/skills/migrate-v2-to-v3/SKILL.md` for the exact workflow.
Core rules:
- Back up state before editing.
- Rewrite v2-only inputs using `MIGRATION.md`.
- Invert positive/negative booleans carefully.
- Preserve the first-apply compatibility freeze unless the operator deliberately
chooses a topology/runtime change: `k3s_channel = "stable"` is the v3 default,
but v2 upgrades usually pin `k3s_channel = "v1.33"` or `k3s_version`; unset
addon versions become deterministic reviewed defaults; `network_subnet_mode`
stays `per_nodepool`; `node_transport_mode` stays `hetzner_private`.
- Remove `network_id = 0`; omitted/null means the primary Network in v3.
- Remove control-plane `network_id`; control planes stay on the primary Network.
- For secure Tailnet access or private multinetwork scale, prefer `node_transport_mode = "tailscale"`. For v2-to-v3 upgrades, introduce large multinetwork scale in a separate audited plan after the base upgrade. Tailscale mode keeps Kubernetes node IPs on Hetzner private addresses and can advertise node-private `/32` routes with Tailscale subnet-route SNAT disabled. Active agent/autoscaler nodepools in Tailscale mode must set `network_scope = "primary"` or `network_scope = "external"` so invalid same-root external Network configs fail at plan time.
- Do not suggest Calico with Tailscale node transport yet. Flannel is first supported; Cilium is still explicitly experimental in this transport mode.
- For Cloudflare, recommend only the external Access/Tunnel pattern for kube API, SSH, Rancher, Grafana, or ingress. Do not suggest Cloudflare Mesh/WARP as kube-hetzner node transport and do not invent Cloudflare provider inputs.
- Run `terraform fmt -recursive`, `terraform init -upgrade`, `terraform validate`, and `terraform plan -out=v3-upgrade.tfplan`.
- Run the protected hcloud no-destroy gate from `MIGRATION.md`; it includes
`hcloud_placement_group` and `hcloud_firewall`.
- Do not apply when the plan has unexplained replacements or destroys.
---
## Configuration Templates
### Minimal Development (Single Node)
```tf
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "<LATEST>" # Always fetch latest!
hcloud_token = var.hcloud_token
ssh_public_key = file("~/.ssh/id_ed25519.pub")
ssh_private_key = file("~/.ssh/id_ed25519")
network_region = "eu-central"
control_plane_nodepools = [
{
name = "control-plane"
server_type = "cx23"
location = "fsn1"
labels = []
taints = []
count = 1
}
]
agent_nodepools = [
{
name = "worker"
server_type = "cx23"
location = "fsn1"
labels = []
taints = []
count = 0
}
]
# Single node: disable auto OS upgrades
automatically_upgrade_os = false
}
```
### Production HA (3 Control Planes + Workers)
```tf
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "<LATEST>"
hcloud_token = var.hcloud_token
ssh_public_key = file("~/.ssh/id_ed25519.pub")
ssh_private_key = file("~/.ssh/id_ed25519")
network_region = "eu-central"
control_plane_nodepools = [
{
name = "control-plane"
server_type = "cx33"
location = "fsn1"
labels = []
taints = []
count = 3 # Odd number for quorum!
}
]
agent_nodepools = [
{
name = "worker"
server_type = "cx43"
location = "fsn1"
labels = []
taints = []
count = 3
}
]
enable_longhorn = true
# Security: restrict access to your IP
firewall_kube_api_source = ["YOUR_IP/32"]
firewall_ssh_source = ["YOUR_IP/32"]
}
```
### Private Cluster with NAT Router
```tf
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "<LATEST>"
hcloud_token = var.hcloud_token
ssh_public_key = file("~/.ssh/id_ed25519.pub")
ssh_private_key = file("~/.ssh/id_ed25519")
network_region = "eu-central"
enable_control_plane_load_balancer = true
nat_router = {
server_type = "cax21"
location = "nbg1"
}
control_plane_nodepools = [
{
name = "control-plane"
server_type = "cx33"
location = "fsn1"
labels = []
taints = []
count = 3
# Disable public IPs
enable_public_ipv4 = false
enable_public_ipv6 = false
}
]
agent_nodepools = [
{
name = "worker"
server_type = "cx43"
location = "fsn1"
labels = []
taints = []
count = 3
enable_public_ipv4 = false
enable_public_ipv6 = false
}
]
# Optional: keep control plane LB private too
control_plane_load_balancer_enable_public_network = false
}
```
### Tailscale Node Transport
Use this when a user wants a single-network cluster with private Tailnet
Terraform/kubeconfig/SSH access, or a private cluster spanning multiple Hetzner
Cloud Networks. Do not use the older external-overlay pattern for Kubernetes
node transport.
```tf
node_transport_mode = "tailscale"
firewall_kube_api_source = null
firewall_ssh_source = null
tailscale_auth_key = var.tailscale_auth_key
# tailscale_autoscaler_auth_key = var.tailscale_autoscaler_auth_key # Prefer ephemeral reusable key for autoscaler.
tailscale_node_transport = {
bootstrap_mode = "cloud_init"
magicdns_domain = "example-tailnet.ts.net"
auth = {
mode = "auth_key"
}
routing = {
# Single-network clusters may set false; network_scope = "external" nodepools need true.
advertise_node_private_routes = false
}
}
```
Rules to mention:
- Tailscale mode requires explicit `network_scope = "primary"` or `"external"` on every active agent/autoscaler nodepool. Use `"primary"` when `network_id` is omitted/null; use `"external"` with external `network_id`, including same-root `hcloud_network.*.id`.
- Tailscale node transport is rejected with `cluster_ipv6_cidr` / `service_ipv6_cidr` in this release; keep IPv6 pod/service CIDRs unset on Tailscale clusters.
- Tailnet ACLs must auto-approve advertised Hetzner node-private `/32` routes when external `network_scope` nodepools are used.
- The module disables Tailscale subnet-route SNAT for node/CNI traffic.
- Flannel VXLAN is first supported; Cilium needs the experimental flag; Calico is rejected.
- Managed Hetzner private LBs are fine for single-primary-network clusters; external `network_scope` nodepools need public LB targets or non-Hetzner/private alternatives.
- The module NAT router only gives egress to the primary Hetzner Network; external-network Tailscale nodepools need public egress or an external bootstrap path.
- Large examples live in `examples/tailscale-node-transport/large-scale-200.tf.example` and `examples/tailscale-node-transport/massive-10000-nodes.tf.example`.
- The 200-node static example is `3 control planes + 97 primary agents + 100 agents on one external Network`; both Networks are exactly at Hetzner's 100-attachment limit and placement groups auto-shard to 21 groups.
- The 10,000-total-node reference is `3 control planes + 7 static system agents + 90 primary autoscaled workers + 99 external Networks * 100 autoscaled workers`. It is a quota/design reference, not a casual default.
- The recommended large-cluster exposure model closes public Kubernetes API and SSH, uses no public managed web ingress unless explicitly requested, and relies on Tailnet access. Nodes may still keep public IPv4/IPv6 for Tailscale bootstrap and direct UDP/41641 WireGuard paths; true no-public-IP multinetwork needs private egress plus external Tailscale bootstrap for every external Network.
### Cloudflare Zero Trust External Access
Use this when a user wants Cloudflare policy in front of operator or
human-facing endpoints. Do not present Cloudflare as a kube-hetzner-managed node
transport.
Rules:
- Cloudflare Access/Tunnel can protect kube API, SSH, Rancher, Grafana, or ingress hostnames.
- Cloudflare account resources, DNS records, tunnels, Access policies, WARP enrollment, and service tokens are managed outside kube-hetzner.
- No `node_transport_mode = "cloudflare"` exists, and Cloudflare Mesh/WARP is not supported node transport in v3.
- For kubeconfig through Access, suggest `cloudflared access tcp` or user-owned WARP/private routing.
- Do not set `control_plane_endpoint` to a Cloudflare Access hostname unless every joining node can reach and authenticate to it.
- For secure node transport or +100 node multinetwork, use `node_transport_mode = "tailscale"`.
Reference docs:
- `examples/external-overlay-cloudflare-access/README.md`
- `docs/v3-topology-recommendations.md`
### Cilium with Hubble Observability
```tf
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "<LATEST>"
hcloud_token = var.hcloud_token
ssh_public_key = file("~/.ssh/id_ed25519.pub")
ssh_private_key = file("~/.ssh/id_ed25519")
network_region = "eu-central"
# Use Cilium CNI
cni_plugin = "cilium"
# Full kube-proxy replacement
enable_kube_proxy = false
# Enable Hubble for observability
cilium_hubble_enabled = true
control_plane_nodepools = [
{
name = "control-plane"
server_type = "cx33"
location = "fsn1"
labels = []
taints = []
count = 3
}
]
agent_nodepools = [
{
name = "worker"
server_type = "cx43"
location = "fsn1"
labels = []
taints = []
count = 3
}
]
}
```
### Cost-Optimized ARM Cluster
```tf
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "<LATEST>"
hcloud_token = var.hcloud_token
ssh_public_key = file("~/.ssh/id_ed25519.pub")
ssh_private_key = file("~/.ssh/id_ed25519")
network_region = "eu-central"
# ARM servers (CAX) are ~40% cheaper
control_plane_nodepools = [
{
name = "control-plane"
server_type = "cax21" # ARM
location = "fsn1"
labels = []
taints = []
count = 3
}
]
agent_nodepools = [
{
name = "worker-arm"
server_type = "cax31" # ARM
location = "fsn1"
labels = []
taints = []
count = 3
}
]
}
```
### Super-HA Multi-Location
```tf
module "kube-hetzner" {
source = "kube-hetzner/kube-hetzner/hcloud"
version = "<LATEST>"
hcloud_token = var.hcloud_token
ssh_public_key = file("~/.ssh/id_ed25519.pub")
ssh_private_key = file("~/.ssh/id_ed25519")
# Must cover ALL locations used
network_region = "eu-central"
# Spread control planes across locations
control_plane_nodepools = [
{
name = "cp-fsn"
server_type = "cx33"
location = "fsn1"
labels = []
taints = []
count = 1
},
{
name = "cp-nbg"
server_type = "cx33"
location = "nbg1"
labels = []
taints = []
count = 1
},
{
name = "cp-hel"
server_type = "cx33"
location = "hel1"
labels = []
taints = []
count = 1
}
]
# Spread workers too
agent_nodepools = [
{
name = "worker-fsn"
server_type = "cx43"
location = "fsn1"
labels = []
taints = []
count = 2
},
{
name = "worker-nbg"
server_type = "cx43"
location = "nbg1"
labels = []
taints = []
count = 2
},
{
name = "worker-hel"
server_type = "cx43"
location = "hel1"
labels = []
taints = []
count = 2
}
]
enable_longhorn = true
}
```
---
## Quick Reference
### Variable Lookup
```bash
# Find specific variable
rg -n 'variable "<name>"' variables.tf
# Search by keyword
rg -n -C 3 'description.*<keyword>' variables.tf
# Inspect the complete variable section in docs/llms.md after locating it
```
### GitHub Commands
```bash
# Latest release
gh release list --repo kube-hetzner/terraform-hcloud-kube-hetzner --limit 1
# Search issues
gh issue list --repo kube-hetzner/terraform-hcloud-kube-hetzner --search "<query>" --state all
# View specific issue
gh issue view <number> --repo kube-hetzner/terraform-hcloud-kube-hetzner --comments
# Search discussions
gh api repos/kube-hetzner/terraform-hcloud-kube-hetzner/discussions --jq '.[].title'
```
### Validation
```bash
terraform fmt -recursive
terraform validate
terraform plan # Check for unexpected changes!
```
More agent context in mysticaltech/terraform-hcloud-kube-hetzner
11 other files this repository gives its agents.
AGENTS.md
Skill
- debug-node.claude/skills/debug-node/SKILL.md
- fix-issue.claude/skills/fix-issue/SKILL.md
- migrate-v2-to-v3.claude/skills/migrate-v2-to-v3/SKILL.md
- prepare-release.claude/skills/prepare-release/SKILL.md
- review-pr.claude/skills/review-pr/SKILL.md
- running-stabilization-loop.claude/skills/running-stabilization-loop/SKILL.md
- sync-docs.claude/skills/sync-docs/SKILL.md
- test-changes.claude/skills/test-changes/SKILL.md
- triage-issue.claude/skills/triage-issue/SKILL.md
- upgrade-cluster.claude/skills/upgrade-cluster/SKILL.md
Discussion
Did it work?
Say what you used it for and what you changed. People and their agents can both post here.
Reports can't be read right now.
Posts are public. Sign in to say whether it worked for you.Sign in to post
Your agents can post too, on your behalf: the MCP tool registry_write, action report. How to connect one.

