feat: Add HA Kubernetes cluster with Terraform + Ansible
Some checks failed
Terraform / Validate (push) Failing after 17s
Terraform / Plan (push) Has been skipped
Terraform / Apply (push) Has been skipped

- 3x CX23 control plane nodes (HA)
- 4x CX33 worker nodes
- k3s with embedded etcd
- Hetzner CCM for load balancers
- Gitea CI/CD workflows
- Backblaze B2 for Terraform state
This commit is contained in:
2026-02-28 20:24:55 +00:00
parent 3e8eb072b5
commit 3b3084b997
27 changed files with 1324 additions and 0 deletions

10
terraform/backend.tf Normal file
View File

@@ -0,0 +1,10 @@
terraform {
backend "s3" {
key = "terraform.tfstate"
region = "auto"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
force_path_style = true
}
}

86
terraform/firewall.tf Normal file
View File

@@ -0,0 +1,86 @@
resource "hcloud_firewall" "cluster" {
name = "${var.cluster_name}-firewall"
rule {
description = "SSH"
direction = "in"
protocol = "tcp"
port = "22"
source_ips = var.allowed_ssh_ips
}
rule {
description = "Kubernetes API"
direction = "in"
protocol = "tcp"
port = "6443"
source_ips = var.allowed_api_ips
}
rule {
description = "Kubernetes API (internal)"
direction = "in"
protocol = "tcp"
port = "6443"
source_ips = [var.subnet_cidr]
}
rule {
description = "k3s Supervisor"
direction = "in"
protocol = "tcp"
port = "9345"
source_ips = [var.subnet_cidr]
}
rule {
description = "etcd Client"
direction = "in"
protocol = "tcp"
port = "2379"
source_ips = [var.subnet_cidr]
}
rule {
description = "etcd Peer"
direction = "in"
protocol = "tcp"
port = "2380"
source_ips = [var.subnet_cidr]
}
rule {
description = "Flannel VXLAN"
direction = "in"
protocol = "udp"
port = "8472"
source_ips = [var.subnet_cidr]
}
rule {
description = "Kubelet"
direction = "in"
protocol = "tcp"
port = "10250"
source_ips = [var.subnet_cidr]
}
rule {
description = "NodePorts"
direction = "in"
protocol = "tcp"
port = "30000-32767"
source_ips = ["0.0.0.0/0"]
}
rule {
description = "ICMP"
direction = "in"
protocol = "icmp"
source_ips = ["0.0.0.0/0"]
}
apply_to {
label_selector = "cluster=${var.cluster_name}"
}
}

14
terraform/main.tf Normal file
View File

@@ -0,0 +1,14 @@
terraform {
required_version = ">= 1.0"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}

11
terraform/network.tf Normal file
View File

@@ -0,0 +1,11 @@
resource "hcloud_network" "cluster" {
name = "${var.cluster_name}-network"
ip_range = var.network_cidr
}
resource "hcloud_network_subnet" "servers" {
network_id = hcloud_network.cluster.id
type = "cloud"
network_zone = "${var.location}-network"
ip_range = var.subnet_cidr
}

44
terraform/outputs.tf Normal file
View File

@@ -0,0 +1,44 @@
output "control_plane_ips" {
description = "Public IPs of control plane nodes"
value = [for cp in hcloud_server.control_plane : cp.ipv4_address]
}
output "control_plane_private_ips" {
description = "Private IPs of control plane nodes"
value = [for cp in hcloud_server.control_plane : cp.network[0].ip]
}
output "primary_control_plane_ip" {
description = "Public IP of the primary control plane (first node)"
value = hcloud_server.control_plane[0].ipv4_address
}
output "worker_ips" {
description = "Public IPs of worker nodes"
value = [for worker in hcloud_server.workers : worker.ipv4_address]
}
output "worker_private_ips" {
description = "Private IPs of worker nodes"
value = [for worker in hcloud_server.workers : worker.network[0].ip]
}
output "ssh_private_key_path" {
description = "Path to SSH private key"
value = var.ssh_private_key
}
output "cluster_name" {
description = "Cluster name"
value = var.cluster_name
}
output "network_cidr" {
description = "Private network CIDR"
value = var.subnet_cidr
}
output "kubeconfig_command" {
description = "Command to fetch kubeconfig"
value = "ssh root@${hcloud_server.control_plane[0].ipv4_address} 'cat /etc/rancher/k3s/k3s.yaml' > kubeconfig && sed -i 's/127.0.0.1/${hcloud_server.control_plane[0].ipv4_address}/g' kubeconfig"
}

60
terraform/servers.tf Normal file
View File

@@ -0,0 +1,60 @@
data "hcloud_image" "ubuntu" {
name = "ubuntu-24.04"
with_status = ["available"]
}
resource "hcloud_server" "control_plane" {
count = var.control_plane_count
name = "${var.cluster_name}-cp-${count.index + 1}"
server_type = var.control_plane_type
image = data.hcloud_image.ubuntu.id
location = var.location
ssh_keys = [hcloud_ssh_key.cluster.id]
labels = {
cluster = var.cluster_name
role = "control-plane"
}
network {
network_id = hcloud_network.cluster.id
ip = cidrhost(var.subnet_cidr, 10 + count.index)
}
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
firewall_ids = [hcloud_firewall.cluster.id]
}
resource "hcloud_server" "workers" {
count = var.worker_count
name = "${var.cluster_name}-worker-${count.index + 1}"
server_type = var.worker_type
image = data.hcloud_image.ubuntu.id
location = var.location
ssh_keys = [hcloud_ssh_key.cluster.id]
labels = {
cluster = var.cluster_name
role = "worker"
}
network {
network_id = hcloud_network.cluster.id
ip = cidrhost(var.subnet_cidr, 20 + count.index)
}
public_net {
ipv4_enabled = true
ipv6_enabled = true
}
firewall_ids = [hcloud_firewall.cluster.id]
depends_on = [hcloud_server.control_plane]
}

8
terraform/ssh.tf Normal file
View File

@@ -0,0 +1,8 @@
data "local_file" "ssh_public_key" {
filename = pathexpand(var.ssh_public_key)
}
resource "hcloud_ssh_key" "cluster" {
name = "${var.cluster_name}-ssh-key"
public_key = data.local_file.ssh_public_key.content
}

100
terraform/variables.tf Normal file
View File

@@ -0,0 +1,100 @@
variable "hcloud_token" {
description = "Hetzner Cloud API token"
type = string
sensitive = true
}
variable "ssh_public_key" {
description = "Path to SSH public key"
type = string
default = "~/.ssh/id_ed25519.pub"
}
variable "ssh_private_key" {
description = "Path to SSH private key"
type = string
default = "~/.ssh/id_ed25519"
}
variable "cluster_name" {
description = "Name of the Kubernetes cluster"
type = string
default = "k8s-cluster"
}
variable "control_plane_count" {
description = "Number of control plane nodes"
type = number
default = 3
}
variable "control_plane_type" {
description = "Hetzner server type for control plane"
type = string
default = "cx23"
}
variable "worker_count" {
description = "Number of worker nodes"
type = number
default = 4
}
variable "worker_type" {
description = "Hetzner server type for workers"
type = string
default = "cx33"
}
variable "location" {
description = "Hetzner datacenter location"
type = string
default = "fsn1"
}
variable "allowed_ssh_ips" {
description = "IP ranges allowed for SSH access"
type = list(string)
default = ["0.0.0.0/0"]
}
variable "allowed_api_ips" {
description = "IP ranges allowed for Kubernetes API access"
type = list(string)
default = ["0.0.0.0/0"]
}
variable "network_cidr" {
description = "CIDR for private network"
type = string
default = "10.0.0.0/16"
}
variable "subnet_cidr" {
description = "CIDR for server subnet"
type = string
default = "10.0.1.0/24"
}
variable "s3_access_key" {
description = "S3 access key for Terraform state"
type = string
sensitive = true
}
variable "s3_secret_key" {
description = "S3 secret key for Terraform state"
type = string
sensitive = true
}
variable "s3_endpoint" {
description = "S3 endpoint URL"
type = string
}
variable "s3_bucket" {
description = "S3 bucket name for Terraform state"
type = string
default = "k8s-terraform-state"
}