feat: Add HA Kubernetes cluster with Terraform + Ansible
- 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:
10
terraform/backend.tf
Normal file
10
terraform/backend.tf
Normal 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
86
terraform/firewall.tf
Normal 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
14
terraform/main.tf
Normal 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
11
terraform/network.tf
Normal 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
44
terraform/outputs.tf
Normal 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
60
terraform/servers.tf
Normal 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
8
terraform/ssh.tf
Normal 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
100
terraform/variables.tf
Normal 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"
|
||||
}
|
||||
Reference in New Issue
Block a user