A k0s Kubernetes cluster running on Hetzner Cloud, provisioned with Terraform and bootstrapped with k0sctl. Infrastructure changes are applied automatically via GitHub Actions on merge to main.
| Node | Type | Role |
|---|---|---|
k0s-controller |
cx23 | k0s control plane |
k0s-worker |
cx23 | k0s worker |
Both nodes are placed in a private network (10.10.0.0/24) for intra-cluster traffic and share a firewall that restricts inbound access to SSH and the Kubernetes API.
PR opened
└─ test job: fmt → validate → plan (posted as PR comment)
Merge to main
└─ test job: fmt → validate → plan
└─ apply job: terraform apply
└─ k0sctl workflow: k0s bootstrap → kubeconfig artifact
GCP authentication uses Workload Identity Federation (OIDC) — no long-lived service account keys anywhere.
brew install terraform k0sproject/tap/k0sctl kubectlCopy and fill in the vars file:
cp terraform.tfvars.example terraform.tfvars
# Set hcloud_token, ssh_public_key, ssh_private_key_pathInitialize with the remote GCS backend:
terraform initmake plan # preview changes
make apply # create/update Hetzner nodesAfter apply writes k0sctl.yaml with the real node IPs:
make bootstrap # installs k0sctl if needed, then runs k0sctl applymake kubeconfig
export KUBECONFIG=$PWD/k0s-kubeconfig.yaml
kubectl get nodesmake ssh-controller
make ssh-workermake destroyThe following secrets must be set in the GitHub repo (Settings → Secrets → Actions):
| Secret | Description |
|---|---|
HCLOUD_TOKEN |
Hetzner Cloud API token |
SSH_PUBLIC_KEY |
Public key uploaded to Hetzner nodes |
SSH_PRIVATE_KEY |
Private key used by k0sctl to SSH into nodes |
GCP_WORKLOAD_IDENTITY_PROVIDER |
WIF provider resource name (from bootstrap script) |
GCP_SERVICE_ACCOUNT |
Service account email for GCS state access |
Run once to create the GCS state bucket and configure Workload Identity Federation:
./scripts/bootstrap-gcp.sh didiberman/k0sdev-labTerraform state is stored in GCS: gs://didiberman-tfstate/k0s-exp.
.
├── main.tf # Hetzner provider, servers, network, firewall
├── variables.tf # Input variables
├── outputs.tf # Node IPs, SSH shortcuts
├── backend.tf # GCS remote state
├── k0sctl.yaml.tpl # k0s cluster config template (rendered by Terraform)
├── terraform.tfvars.example # Example vars (copy to terraform.tfvars locally)
├── Makefile # Local workflow shortcuts
├── scripts/
│ └── bootstrap-gcp.sh # One-time GCP setup (bucket + WIF)
└── .github/workflows/
├── terraform.yml # fmt + validate + plan on PR; apply on main
└── k0sctl.yml # k0s bootstrap after successful apply