CtrlK
BlogDocsLog inGet started
Tessl Logo

kh-assistant

Use when users need help with kube-hetzner configuration, debugging, or questions - acts as an intelligent assistant with live repo access

63

Quality

76%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Fix and improve this skill with Tessl

tessl review fix ./.claude/skills/kh-assistant/SKILL.md
SKILL.md
Quality
Evals
Security

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:

# 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 Gemini for 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):

# 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 intentSkillWhat it doesInvocation
Migrate an existing Terraform root or cluster from module v2.x to v3.xmigrate-v2-to-v3Audits 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 safelyupgrade-clusterSeparates module convergence from runtime rollout and proves Terraform plus Kubernetes health/upgrade-cluster
Diagnose an unreachable node, SSH/cloud-init failure, or stuck provisioningdebug-nodeUses Hetzner rescue mode to mount and inspect the node without working node SSH/debug-node
Validate module changes with Terraform and OpenTofutest-changesRuns formatting, validation, compatibility, example, and plan gates against a supplied test root/test-changes
Implement a GitHub issue (maintainer only)fix-issueFetches 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-issueChecks evidence and duplicates, classifies the report, and drafts the appropriate response/action/triage-issue <number>
Review a pull request (maintainer only)review-prPerforms a security, compatibility, regression, and code-quality review of an untrusted contribution/review-pr <number>
Synchronize project documentation (maintainer only)sync-docsKeeps variables.tf, generated/reference docs, examples, migration docs, and skills coherent/sync-docs
Prepare or execute a release (maintainer only)prepare-releaseVerifies 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-loopIteratively 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:

# 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

FilePurposeWhen to Use
docs/llms.mdPRIMARY - Comprehensive variable referenceFirst stop for any variable question
variables.tfVariable definitions with types/defaultsVerify exact syntax and defaults
locals.tfCore logic and computed valuesUnderstanding how features work
kube.tf.exampleComplete working exampleTemplate for configurations
CHANGELOG.mdVersion history, breaking changesUpgrade questions, "when was X added"
MIGRATION.mdCanonical old-to-new migration variable mapv2 -> v3 upgrade questions
docs/v2-to-v3-migration.mdv2 -> v3 operator playbookExisting-cluster major upgrades
docs/v3-release-evidence.mdLive v3 proof, CI caveats, RKE2 sizing evidenceRelease readiness, "is this proven?" questions
docs/v3-topology-recommendations.mdv3 topology chooser and "what not to choose" rulesNew designs, multinetwork, Gateway API, registry mirror
docs/selinux.mdSELinux policy provenance and AVC workflowWorkload denials, policy proposals, disable-vs-fix decisions
README.mdProject overview, quick startNew user orientation

Specialized Documentation

FileTopic
docs/terraform.mdAuto-generated terraform docs
docs/ssh.mdSSH configuration, key formats
docs/add-robot-server.mdHetzner dedicated server integration
docs/private-network-egress.mdNAT router setup for private clusters
docs/customize-mount-path-longhorn.mdLonghorn storage customization

GitHub (Live Data)

# 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.0.1.

FactCurrent contract
Kubernetes distributionk3s is the default; RKE2 is supported via kubernetes_distribution = "rke2"
Kubernetes version policyk3s defaults to the upstream stable channel; pin a version/channel when reproducibility or v2 minor preservation matters
Node OSBrand-new nodepools default to Leap Micro; existing MicroOS nodepools remain supported and are preserved on normal v2 upgrades
Addon versionsUnset 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

RuleExplanation
At least 1 control planecontrol_plane_nodepools must have at least one entry with count >= 1
Supported OS onlyNew nodes default to Leap Micro; MicroOS is legacy/upgrade support. Never suggest Ubuntu, Debian, or other generic OS images.
Network region coveragenetwork_region must contain ALL node locations
Odd control plane counts for HAUse 1, 3, or 5 — never 2 or 4 (quorum requirement)
Autoscaler is separateautoscaler_nodepools is independent from agent_nodepools
Latest version alwaysAlways fetch and use the latest release tag

Common Mistakes to Prevent

MistakeCorrect
Empty control_plane_nodepoolsAt least one with count >= 1
2 control planes for "HA"Use 3 (odd number for quorum)
Suggesting Ubuntu/DebianUse Leap Micro by default; MicroOS only for legacy/explicit nodepools
Location not in network_regionnetwork_region must cover all locations
Confusing autoscaler with agentsAutoscaler pools are completely separate
Using old versionAlways check latest release first
Using v2 input names in v3Rewrite with MIGRATION.md: enable_* booleans, kubernetes_distribution, k3s_channel, rke2_channel, node_transport_mode, and network_subnet_mode
Raw Hetzner private multinetwork for >100 nodesUse 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 transportUse node_transport_mode = "tailscale" for cluster transport; use node_connection_overrides only for user-owned operator access
Treating Cloudflare Mesh/WARP as supported node transportUse Tailscale for kube-hetzner-managed secure node transport; Cloudflare Access/Tunnel is external operator/app access only
Assuming one Hetzner Network can exceed 100 nodesShard 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 projectHetzner 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 providerUse cilium_gateway_api_enabled for Cilium, traefik_provider_kubernetes_gateway_enabled for Traefik
Enabling Cilium Gateway API with kube-proxyRequires cni_plugin = "cilium" and enable_kube_proxy = false
Enabling embedded registry mirror on low-trust nodesUse only for equal-trust clusters; warn about credential sharing and tag poisoning
Disabling SELinux globally for one workload denialFollow docs/selinux.md: collect AVCs, try udica, use per-pool selinux = false only as the last resort
Assuming RKE2 needs 8GB control planesv3 size-aware kubelet reservations make 4GB cx23 control planes viable; still size production for workload headroom
Manual cloud deletes during teardownUse scripts/destroy.sh first; scripts/cleanup.sh is the forceful fallback

v3 Topology Shortcuts

NeedRecommendation
Small devSingle control plane, one small agent pool, no ingress unless needed
Normal HA3 control planes, 2+ agents, one primary Hetzner Network
Private-onlyNAT router and private control-plane LB on the primary Network
Secure API/SSHnode_transport_mode = "tailscale" and close public API/SSH firewall sources
Cloudflare-protected operator/app accessUser-managed Cloudflare Access/Tunnel in front of kube API, SSH, Rancher, Grafana, or ingress; keep node transport as Hetzner private or Tailscale
+100 Cloud nodesTailscale node transport plus one external Hetzner Network shard per 100-node budget
10k referenceAutoscaler-first Tailscale multinetwork; point to examples/tailscale-node-transport/massive-10000-nodes.tf.example
Cilium Gateway APICilium, enable_kube_proxy = false, cilium_gateway_api_enabled = true
Heavy image pullsembedded_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; 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 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

ErrorCauseSolution
cannot sum empty listcontrol_plane_nodepools is empty or all counts are 0Add at least one control plane with count >= 1
NAT router primary IPs will be replacedPre-v2.19.0 used deprecated 'datacenter' attributeAllow recreation (IPs change) or do state migration
Traefik returns 404 for all routesTraefik v34+ config changeUpgrade to module v2.19.0+
SSH connection refused or timeoutKey format, firewall, or node not readyCheck ssh_public_key format, verify firewall_ssh_source
Node stuck in NotReadyNetwork region mismatch or token issuesEnsure network_region contains all node locations
Error creating network subnetSubnet CIDR conflictsCheck network_ipv4_cidr doesn't overlap with existing
cloud-init failedLeap Micro/MicroOS snapshot missing, wrong region, wrong architecture, or wrong distro labelRecreate snapshots with packer in the correct region/architecture and k3s/RKE2 SELinux variant
resource_already_detaching or LB network 422 during destroyKnown benign ingress-LB detach race between CCM and Terraform ownershipRun scripts/destroy.sh; it retries only this race and then prints an orphan report
Network/subnet destroy hangs with autoscaler enabledAutoscaler-created servers are not in Terraform state and still pin the networkWait until the control plane is dead, or scale autoscaler min_nodes = 0, then delete the orphan
SELinux avc: denied workload failuresMissing workload policy, not automatically a module bugFollow 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)

TypevCPURAMDiskBest For
cx2324GB40GBMinimal dev, small k3s/RKE2 control planes
cx3348GB80GBProduction control plane, moderate workers
cx43816GB160GBProduction workers
cx531632GB320GBHeavy 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)

TypevCPURAMDiskBest For
cax1124GB40GBARM dev
cax2148GB80GBARM workloads
cax31816GB160GBARM production
cax411632GB320GBARM heavy

Locations

RegionLocationsNetwork Zone
Germanyfsn1, nbg1eu-central
Finlandhel1eu-central
USA Eastashus-east
USA Westhilus-west
Singaporesinap-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

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; review its dry run before deletion.

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)

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)

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

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.

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.
  • 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

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

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

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

# Find specific variable
rg -n 'variable "<name>"' variables.tf

# Search by keyword
rg -n -C 3 'description.*<keyword>' variables.tf

# Use Gemini for comprehensive search
gemini --model gemini-3.1-pro-preview -p "@docs/llms.md Explain the <variable_name> variable"

GitHub Commands

# 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

terraform fmt -recursive
terraform validate
terraform plan  # Check for unexpected changes!
Repository
mysticaltech/terraform-hcloud-kube-hetzner
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.