## 📋 1. PREREQUISITES
- Ubuntu/Debian server (or your local Linux)
- Hetzner Cloud account
- SSH access
---
## 🔑 2. GETTING HETZNER API TOKEN
1. Go to https://console.hetzner.cloud/
2. Select or create a project
3. Security → API Tokens → Generate API Token
4. Name: `terraform-homelab`
5. Permissions: **Read & Write**
6. **Copy the token** (shown only once!)
---
## ⚡ 3. AUTOMATED INSTALLATION (RECOMMENDED)
### 3.1 Download and Run Installer
```bash
# Download the script
curl -O https://raw.githubusercontent.com/yourusername/hetzner-k3s-lab/main/install.sh
# OR create manually (see below)
```
### 3.2 Create the Installer
````bash
cat > ~/install-hetzner-k3s.sh << 'INSTALLER_EOF'
#!/bin/bash
#
# Hetzner K3s Lab - Automated Installer
# Description: Automatically deploys a 3-node K3s cluster on Hetzner Cloud
#
set -e
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
# Configuration
PROJECT_DIR="${HOME}/hetzner-lab"
LOG_FILE="${PROJECT_DIR}/install.log"
# Functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1" | tee -a "$LOG_FILE"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1" | tee -a "$LOG_FILE"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1" | tee -a "$LOG_FILE"
}
log_step() {
echo -e "\n${BLUE}===================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}===================================${NC}\n"
}
check_requirements() {
log_step "Checking Requirements"
if [ "$EUID" -eq 0 ]; then
log_error "Please do not run as root"
exit 1
fi
if ! ping -c 1 google.com &> /dev/null; then
log_error "No internet connection"
exit 1
fi
log_info "Requirements check passed"
}
install_dependencies() {
log_step "Installing Dependencies"
log_info "Updating system packages..."
sudo apt-get update -qq
log_info "Installing basic tools..."
sudo apt-get install -y -qq \
gnupg \
software-properties-common \
curl \
wget \
unzip \
git \
jq \
python3-pip \
> /dev/null 2>&1
if ! command -v terraform &> /dev/null; then
log_info "Installing Terraform..."
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update -qq
sudo apt-get install -y terraform > /dev/null 2>&1
else
log_info "Terraform already installed: $(terraform version | head -n1)"
fi
if ! command -v ansible &> /dev/null; then
log_info "Installing Ansible..."
sudo apt-get install -y ansible > /dev/null 2>&1
else
log_info "Ansible already installed: $(ansible --version | head -n1)"
fi
if ! command -v kubectl &> /dev/null; then
log_info "Installing kubectl..."
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" 2>/dev/null
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
rm kubectl
else
log_info "kubectl already installed"
fi
log_info "All dependencies installed successfully"
}
setup_ssh_key() {
log_step "Setting up SSH Key"
if [ ! -f ~/.ssh/id_rsa ]; then
log_info "Generating SSH key..."
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa -q
log_info "SSH key generated"
else
log_info "SSH key already exists"
fi
}
create_project_structure() {
log_step "Creating Project Structure"
if [ -d "$PROJECT_DIR" ]; then
log_warn "Project directory already exists. Backing up..."
mv "$PROJECT_DIR" "${PROJECT_DIR}.backup.$(date +%s)"
fi
mkdir -p "$PROJECT_DIR"/{terraform/{templates,},ansible/{inventory,roles/{k3s-install/tasks,k3s-reset/tasks},playbooks},scripts}
log_info "Project structure created at: $PROJECT_DIR"
}
create_terraform_files() {
log_step "Creating Terraform Configuration"
cd "$PROJECT_DIR/terraform"
cat > variables.tf << 'EOF'
variable "hcloud_token" {
description = "Hetzner Cloud API Token"
type = string
sensitive = true
}
variable "cluster_name" {
description = "Name of the cluster"
type = string
default = "k3s-homelab"
}
variable "location" {
description = "Hetzner location"
type = string
default = "nbg1"
}
variable "server_type_master" {
description = "Server type for master"
type = string
default = "cx23"
}
variable "server_type_worker" {
description = "Server type for workers"
type = string
default = "cx23"
}
variable "worker_count" {
description = "Number of worker nodes"
type = number
default = 2
}
variable "ssh_public_key_path" {
description = "Path to SSH public key"
type = string
default = "~/.ssh/id_rsa.pub"
}
EOF
cat > main.tf << 'EOF'
terraform {
required_version = ">= 1.5.0"
required_providers {
hcloud = {
source = "hetznercloud/hcloud"
version = "~> 1.45"
}
local = {
source = "hashicorp/local"
version = "~> 2.4"
}
}
}
provider "hcloud" {
token = var.hcloud_token
}
resource "hcloud_ssh_key" "default" {
name = "${var.cluster_name}-key"
public_key = file(pathexpand(var.ssh_public_key_path))
}
resource "hcloud_network" "private" {
name = "${var.cluster_name}-network"
ip_range = "10.0.0.0/16"
}
resource "hcloud_network_subnet" "subnet" {
network_id = hcloud_network.private.id
type = "cloud"
network_zone = "eu-central"
ip_range = "10.0.1.0/24"
}
resource "hcloud_firewall" "k3s" {
name = "${var.cluster_name}-firewall"
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "6443"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = ["0.0.0.0/0", "::/0"]
}
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
}
resource "hcloud_server" "k3s_master" {
name = "${var.cluster_name}-master"
server_type = var.server_type_master
image = "ubuntu-22.04"
location = var.location
ssh_keys = [hcloud_ssh_key.default.id]
firewall_ids = [hcloud_firewall.k3s.id]
network {
network_id = hcloud_network.private.id
ip = "10.0.1.10"
}
user_data = <<-EOT
#cloud-config
hostname: ${var.cluster_name}-master
package_update: true
package_upgrade: true
packages:
- curl
- wget
- git
- htop
- vim
- jq
timezone: UTC
runcmd:
- swapoff -a
- sed -i '/ swap / s/^/#/' /etc/fstab
EOT
labels = {
type = "k3s-master"
cluster = var.cluster_name
}
depends_on = [hcloud_network_subnet.subnet]
}
resource "hcloud_server" "k3s_workers" {
count = var.worker_count
name = "${var.cluster_name}-worker-${count.index + 1}"
server_type = var.server_type_worker
image = "ubuntu-22.04"
location = var.location
ssh_keys = [hcloud_ssh_key.default.id]
firewall_ids = [hcloud_firewall.k3s.id]
network {
network_id = hcloud_network.private.id
ip = "10.0.1.${20 + count.index}"
}
user_data = <<-EOT
#cloud-config
hostname: ${var.cluster_name}-worker-${count.index + 1}
package_update: true
package_upgrade: true
packages:
- curl
- wget
- git
- htop
- vim
timezone: UTC
runcmd:
- swapoff -a
- sed -i '/ swap / s/^/#/' /etc/fstab
EOT
labels = {
type = "k3s-worker"
cluster = var.cluster_name
}
depends_on = [hcloud_network_subnet.subnet]
}
resource "local_file" "ansible_inventory" {
content = templatefile("${path.module}/templates/inventory.tpl", {
master_ip = hcloud_server.k3s_master.ipv4_address
worker_ips = [for w in hcloud_server.k3s_workers : w.ipv4_address]
})
filename = "${path.root}/../ansible/inventory/hosts.yml"
}
resource "local_file" "server_ips" {
content = jsonencode({
master_ip = hcloud_server.k3s_master.ipv4_address
worker_ips = [for w in hcloud_server.k3s_workers : w.ipv4_address]
})
filename = "${path.root}/../server_ips.json"
}
EOF
cat > outputs.tf << 'EOF'
output "master_ip" {
description = "Master node public IP"
value = hcloud_server.k3s_master.ipv4_address
}
output "worker_ips" {
description = "Worker nodes public IPs"
value = [for w in hcloud_server.k3s_workers : w.ipv4_address]
}
output "ssh_master" {
description = "SSH to master"
value = "ssh root@${hcloud_server.k3s_master.ipv4_address}"
}
output "cost_estimate" {
description = "Cost estimates"
value = {
hourly = format("EUR %.4f", (var.worker_count + 1) * 0.005)
two_hours = format("EUR %.4f", (var.worker_count + 1) * 0.005 * 2)
daily = format("EUR %.2f", (var.worker_count + 1) * 0.005 * 24)
monthly = format("EUR %.2f", (var.worker_count + 1) * 2.99)
}
}
EOF
mkdir -p templates
cat > templates/inventory.tpl << 'EOF'
all:
vars:
ansible_user: root
ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
k3s_version: v1.28.5+k3s1
k3s_token: "homelab-secret-token-12345"
children:
k3s_cluster:
children:
k3s_master:
hosts:
master:
ansible_host: ${master_ip}
k3s_workers:
hosts:
%{ for idx, ip in worker_ips ~}
worker-${idx + 1}:
ansible_host: ${ip}
%{ endfor ~}
EOF
cat > terraform.tfvars.example << 'EOF'
hcloud_token = "YOUR_HETZNER_API_TOKEN_HERE"
cluster_name = "k3s-homelab"
location = "nbg1"
server_type_master = "cx23"
server_type_worker = "cx23"
worker_count = 2
EOF
log_info "Terraform configuration created"
}
create_ansible_files() {
log_step "Creating Ansible Configuration"
cd "$PROJECT_DIR/ansible"
cat > ansible.cfg << 'EOF'
[defaults]
inventory = ./inventory/hosts.yml
roles_path = ./roles
remote_user = root
host_key_checking = False
retry_files_enabled = False
gathering = smart
timeout = 30
forks = 10
pipelining = True
log_path = ./ansible.log
display_skipped_hosts = False
display_ok_hosts = True
[ssh_connection]
ssh_args = -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r
EOF
cat > roles/k3s-install/tasks/main.yml << 'EOF'
---
- name: Install K3s on master
when: "'k3s_master' in group_names"
block:
- name: Download K3s install script
get_url:
url: https://get.k3s.io
dest: /tmp/k3s-install.sh
mode: '0700'
- name: Get master public IP
set_fact:
master_public_ip: "{{ ansible_host }}"
- name: Install K3s server
shell: |
INSTALL_K3S_VERSION={{ k3s_version }} \
K3S_TOKEN={{ k3s_token }} \
sh /tmp/k3s-install.sh server \
--disable traefik \
--tls-san {{ master_public_ip }} \
--write-kubeconfig-mode 644
args:
creates: /usr/local/bin/k3s
register: k3s_install
- name: Wait for K3s to be ready
wait_for:
port: 6443
host: "{{ master_public_ip }}"
delay: 10
timeout: 300
- name: Wait additional time for API server stability
pause:
seconds: 15
- name: Verify K3s is running
systemd:
name: k3s
state: started
enabled: yes
- name: Test K3s API is responding
command: kubectl get nodes
register: k3s_nodes
retries: 5
delay: 5
until: k3s_nodes.rc == 0
changed_when: false
- name: Fetch kubeconfig
fetch:
src: /etc/rancher/k3s/k3s.yaml
dest: "{{ playbook_dir }}/../kubeconfig"
flat: yes
- name: Install K3s on workers
when: "'k3s_workers' in group_names"
block:
- name: Download K3s install script
get_url:
url: https://get.k3s.io
dest: /tmp/k3s-install.sh
mode: '0700'
- name: Get master public IP from inventory
set_fact:
master_public_ip: "{{ hostvars['master']['ansible_host'] }}"
- name: Display connection info
debug:
msg: "Connecting worker {{ inventory_hostname }} to K3s master at {{ master_public_ip }}:6443"
- name: Install K3s agent
shell: |
K3S_URL=https://{{ master_public_ip }}:6443 \
K3S_TOKEN={{ k3s_token }} \
INSTALL_K3S_VERSION={{ k3s_version }} \
sh /tmp/k3s-install.sh agent
args:
creates: /usr/local/bin/k3s-agent
register: k3s_agent_install
retries: 3
delay: 10
until: k3s_agent_install.rc == 0
- name: Verify K3s agent is running
systemd:
name: k3s-agent
state: started
enabled: yes
- name: Wait for agent to join cluster
pause:
seconds: 10
EOF
cat > roles/k3s-reset/tasks/main.yml << 'EOF'
---
- name: Uninstall K3s from all nodes
block:
- name: Stop K3s services
systemd:
name: "{{ item }}"
state: stopped
loop:
- k3s
- k3s-agent
ignore_errors: yes
- name: Run K3s uninstall script (server)
command: /usr/local/bin/k3s-uninstall.sh
when: "'k3s_master' in group_names"
ignore_errors: yes
- name: Run K3s uninstall script (agent)
command: /usr/local/bin/k3s-agent-uninstall.sh
when: "'k3s_workers' in group_names"
ignore_errors: yes
- name: Clean up remaining files
file:
path: "{{ item }}"
state: absent
loop:
- /etc/rancher
- /var/lib/rancher
- /var/lib/kubelet
- /tmp/k3s-install.sh
ignore_errors: yes
- name: Remove K3s binaries
file:
path: "{{ item }}"
state: absent
loop:
- /usr/local/bin/k3s
- /usr/local/bin/k3s-agent
- /usr/local/bin/kubectl
- /usr/local/bin/crictl
- /usr/local/bin/ctr
ignore_errors: yes
EOF
cat > playbooks/install-k3s.yml << 'EOF'
---
- name: Install K3s cluster
hosts: k3s_cluster
become: yes
roles:
- k3s-install
- name: Configure local kubectl access
hosts: localhost
connection: local
gather_facts: no
tasks:
- name: Ensure .kube directory exists
file:
path: "{{ lookup('env', 'HOME') }}/.kube"
state: directory
mode: '0755'
- name: Copy kubeconfig to local machine
copy:
src: "{{ playbook_dir }}/../kubeconfig"
dest: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner"
mode: '0600'
- name: Get master public IP from inventory
set_fact:
master_ip: "{{ hostvars['master']['ansible_host'] }}"
- name: Update kubeconfig with correct master IP
replace:
path: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner"
regexp: 'https://[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:6443'
replace: "https://{{ master_ip }}:6443"
- name: Also replace localhost in kubeconfig
replace:
path: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner"
regexp: 'https://127\.0\.0\.1:6443'
replace: "https://{{ master_ip }}:6443"
- name: Display kubeconfig setup instructions
debug:
msg:
- "======================================"
- "K3s cluster deployed successfully!"
- "======================================"
- ""
- "To use the cluster:"
- " export KUBECONFIG={{ lookup('env', 'HOME') }}/.kube/config-hetzner"
- " kubectl get nodes"
- ""
- "Or add to ~/.bashrc:"
- " echo 'export KUBECONFIG={{ lookup('env', 'HOME') }}/.kube/config-hetzner' >> ~/.bashrc"
- ""
- "Master IP: {{ master_ip }}"
- ""
- name: Test kubectl connectivity
shell: |
export KUBECONFIG={{ lookup('env', 'HOME') }}/.kube/config-hetzner
kubectl get nodes
register: kubectl_output
changed_when: false
ignore_errors: yes
- name: Display cluster nodes
debug:
msg: "{{ kubectl_output.stdout_lines }}"
when: kubectl_output.rc == 0
- name: Warn if kubectl failed
debug:
msg: "WARNING: kubectl test failed. Please check kubeconfig manually."
when: kubectl_output.rc != 0
EOF
cat > playbooks/reset-k3s.yml << 'EOF'
---
- name: Reset K3s cluster
hosts: k3s_cluster
become: yes
roles:
- k3s-reset
- name: Clean local kubeconfig
hosts: localhost
connection: local
gather_facts: no
tasks:
- name: Remove local kubeconfig
file:
path: "{{ lookup('env', 'HOME') }}/.kube/config-hetzner"
state: absent
- name: Remove project kubeconfig
file:
path: "{{ playbook_dir }}/../kubeconfig"
state: absent
- name: Display reset complete message
debug:
msg:
- "======================================"
- "K3s cluster reset completed"
- "======================================"
EOF
log_info "Ansible configuration created"
}
create_scripts() {
log_step "Creating Management Scripts"
cd "$PROJECT_DIR/scripts"
cat > lab.sh << 'LAB_EOF'
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
BLUE='\033[0;34m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
log_step() {
echo -e "\n${BLUE}===================================${NC}"
echo -e "${BLUE}$1${NC}"
echo -e "${BLUE}===================================${NC}\n"
}
check_token() {
if [ ! -f "$PROJECT_ROOT/terraform/terraform.tfvars" ]; then
log_error "terraform.tfvars not found!"
log_info "Run: cd $PROJECT_ROOT/terraform && cp terraform.tfvars.example terraform.tfvars"
log_info "Then edit terraform.tfvars and add your Hetzner API token"
exit 1
fi
}
start_lab() {
log_step "Starting Hetzner K3s Lab"
check_token
cd "$PROJECT_ROOT/terraform"
log_info "Initializing Terraform..."
terraform init -upgrade
log_info "Planning infrastructure..."
terraform plan -out=tfplan
log_warn "Review the plan above. Continue? (yes/no)"
read -r confirm
if [ "$confirm" != "yes" ]; then
log_error "Cancelled"
exit 0
fi
log_info "Creating infrastructure..."
terraform apply tfplan
log_info "Infrastructure created!"
terraform output
log_info "Waiting 60 seconds for servers to boot..."
sleep 60
log_info "Deploying K3s with Ansible..."
cd "$PROJECT_ROOT/ansible"
log_info "Testing SSH connectivity..."
if ansible all -m ping; then
log_info "SSH connectivity OK"
else
log_error "SSH connectivity failed. Waiting 30 more seconds..."
sleep 30
if ! ansible all -m ping; then
log_error "SSH still failing. Please check manually."
exit 1
fi
fi
log_info "Installing K3s cluster..."
ansible-playbook playbooks/install-k3s.yml
log_step "Lab Ready!"
log_info ""
log_info "To use the cluster:"
log_info " export KUBECONFIG=~/.kube/config-hetzner"
log_info " kubectl get nodes"
log_info ""
log_info "Or add to ~/.bashrc:"
log_info " echo 'export KUBECONFIG=~/.kube/config-hetzner' >> ~/.bashrc"
log_info ""
}
stop_lab() {
log_step "Destroying Hetzner K3s Lab"
log_warn "This will DELETE all servers. Continue? (type 'destroy')"
read -r confirm
if [ "$confirm" != "destroy" ]; then
log_error "Cancelled"
exit 0
fi
if [ -f "$PROJECT_ROOT/ansible/inventory/hosts.yml" ]; then
log_info "Resetting K3s cluster..."
cd "$PROJECT_ROOT/ansible"
ansible-playbook playbooks/reset-k3s.yml || log_warn "K3s reset had some errors (this is OK)"
fi
cd "$PROJECT_ROOT/terraform"
terraform destroy -auto-approve
log_info "Lab destroyed"
}
status_lab() {
cd "$PROJECT_ROOT/terraform"
if [ ! -f "terraform.tfstate" ]; then
log_info "No active lab"
exit 0
fi
log_step "Lab Status"
terraform output
if [ -f "server_ips.json" ]; then
MASTER_IP=$(jq -r '.master_ip' server_ips.json)
log_info "Checking cluster health..."
export KUBECONFIG=~/.kube/config-hetzner
if kubectl get nodes 2>/dev/null; then
log_info "Cluster is healthy"
echo ""
kubectl get nodes
echo ""
kubectl get pods -A
else
log_warn "Cannot connect to cluster via kubectl"
log_info "Checking directly on master node..."
if ssh -o ConnectTimeout=5 root@$MASTER_IP "kubectl get nodes" 2>/dev/null; then
log_info "Cluster is running on master"
else
log_warn "Cannot connect to master"
fi
fi
fi
}
ssh_master() {
cd "$PROJECT_ROOT/terraform"
if [ ! -f "server_ips.json" ]; then
log_error "No active lab"
exit 1
fi
MASTER_IP=$(jq -r '.master_ip' server_ips.json)
ssh root@$MASTER_IP
}
reinstall_k3s() {
log_step "Reinstalling K3s"
cd "$PROJECT_ROOT/ansible"
log_info "Resetting K3s..."
ansible-playbook playbooks/reset-k3s.yml
log_info "Waiting 10 seconds..."
sleep 10
log_info "Reinstalling K3s..."
ansible-playbook playbooks/install-k3s.yml
log_info "K3s reinstalled successfully"
}
show_help() {
echo "Hetzner K3s Lab - Management Script"
echo ""
echo "Usage: $0 {start|stop|status|ssh|reinstall|help}"
echo ""
echo "Commands:"
echo " start - Create infrastructure and deploy K3s cluster"
echo " stop - Destroy cluster and all resources"
echo " status - Show cluster status and information"
echo " ssh - SSH to master node"
echo " reinstall - Reset and reinstall K3s (keep infrastructure)"
echo " help - Show this help message"
echo ""
}
case "${1:-}" in
start)
start_lab
;;
stop)
stop_lab
;;
status)
status_lab
;;
ssh)
ssh_master
;;
reinstall)
reinstall_k3s
;;
help)
show_help
;;
*)
show_help
exit 1
;;
esac
LAB_EOF
chmod +x lab.sh
log_info "Management scripts created"
}
create_readme() {
log_step "Creating Documentation"
cat > "$PROJECT_DIR/README.md" << 'README_EOF'
# Hetzner K3s Lab
Automated 3-node Kubernetes cluster on Hetzner Cloud with Ansible.
## Quick Start
1. **Configure Hetzner API Token**:
```bash
cd ~/hetzner-lab/terraform
cp terraform.tfvars.example terraform.tfvars
nano terraform.tfvars # Add your token
````
2. **Deploy Cluster**:
```bash
cd ~/hetzner-lab/scripts
./lab.sh start
```
3. **Use Cluster**:
```bash
export KUBECONFIG=~/.kube/config-hetzner
kubectl get nodes
```
4. **Destroy Cluster**:
```bash
./lab.sh stop
```
## Commands
```bash
./lab.sh start # Deploy cluster (Terraform + Ansible)
./lab.sh stop # Destroy cluster and all resources
./lab.sh status # Show cluster status
./lab.sh ssh # SSH to master node
./lab.sh reinstall # Reinstall K3s without recreating servers
./lab.sh help # Show help
```
## Features
✅ Fully automated deployment with Terraform + Ansible ✅ Universal kubeconfig management (no manual IP updates) ✅ Automatic retry logic for worker nodes ✅ Clean cluster reset and reinstallation ✅ SSH connectivity testing before deployment ✅ Comprehensive error handling
## Cost
- **Hourly**: EUR 0.015
- **2 hours**: EUR 0.03
- **Daily**: EUR 0.36
- **Monthly**: EUR 8.97
## Architecture
```
Master (CX23) + 2x Workers (CX23)
├── Terraform: Infrastructure provisioning
├── Ansible: K3s installation and configuration
└── Private Network (10.0.0.0/16)
```
## Troubleshooting
If kubectl cannot connect after deployment:
```bash
cd ~/hetzner-lab/scripts
./lab.sh reinstall
```
README_EOF
```
log_info "Documentation created"
```
}
setup_hetzner_token() { log_step "Configuring Hetzner API Token"
```
cd "$PROJECT_DIR/terraform"
cp terraform.tfvars.example terraform.tfvars
log_warn "Please enter your Hetzner API Token:"
read -r HCLOUD_TOKEN
cat > terraform.tfvars << EOF
```
hcloud_token = "$HCLOUD_TOKEN"
cluster_name = "k3s-homelab" location = "nbg1" server_type_master = "cx23" server_type_worker = "cx23" worker_count = 2 EOF
```
log_info "Token configured"
```
}
main() { clear echo -e "${BLUE}" cat << 'BANNER' ╔═══════════════════════════════════════════════════╗ ║ ║ ║ Hetzner K3s Lab - Auto Installer ║ ║ ║ ║ Automated 3-node Kubernetes cluster with ║ ║ Terraform + Ansible (EUR 0.015/hour) ║ ║ ║ ╚═══════════════════════════════════════════════════╝ BANNER echo -e "${NC}\n"
```
mkdir -p "$(dirname "$LOG_FILE")"
check_requirements
install_dependencies
setup_ssh_key
create_project_structure
create_terraform_files
create_ansible_files
create_scripts
create_readme
log_info ""
log_warn "Do you want to configure your Hetzner API token now? (yes/no)"
read -r configure_now
if [ "$configure_now" == "yes" ]; then
setup_hetzner_token
log_info ""
log_warn "Do you want to deploy the cluster now? (yes/no)"
read -r deploy_now
if [ "$deploy_now" == "yes" ]; then
cd "$PROJECT_DIR/scripts"
./lab.sh start
fi
fi
log_step "Installation Complete!"
log_info ""
log_info "Project created at: $PROJECT_DIR"
log_info ""
log_info "Next steps:"
log_info "1. cd $PROJECT_DIR/terraform"
log_info "2. cp terraform.tfvars.example terraform.tfvars"
log_info "3. nano terraform.tfvars # Add your Hetzner API token"
log_info "4. cd ../scripts && ./lab.sh start"
log_info ""
log_info "For help: ./lab.sh help"
log_info ""
```
}
main INSTALLER_EOF
chmod +x ~/install-hetzner-k3s.sh
# Run the installer
~/install-hetzner-k3s.sh
````
---
## 💻 4. USING THE CLUSTER
### Quick Start
```bash
cd ~/hetzner-lab/scripts
# Create cluster (Terraform + Ansible)
./lab.sh start
# Check status
./lab.sh status
# SSH to master
./lab.sh ssh
# Reinstall K3s only (keep servers)
./lab.sh reinstall
# Destroy cluster
./lab.sh stop
````
### Working with kubectl
```bash
# Export kubeconfig (automatically configured by Ansible)
export KUBECONFIG=~/.kube/config-hetzner
# Check nodes
kubectl get nodes
# Check pods
kubectl get pods -A
# Deploy test application
kubectl create deployment nginx --image=nginx --replicas=3
kubectl expose deployment nginx --port=80 --type=NodePort
# Verify
kubectl get pods
kubectl get svc nginx
```
### Monitoring the Cluster
```bash
# Logs
kubectl logs -f <pod-name>
# Events
kubectl get events --sort-by='.lastTimestamp'
# Resource usage (if metrics-server is installed)
kubectl top nodes
kubectl top pods
```
---
## 🗑️ 5. DESTROYING THE CLUSTER
### Via Script (Recommended)
```bash
cd ~/hetzner-lab/scripts
./lab.sh stop
```
This will:
1. Reset K3s cluster with Ansible
2. Destroy all infrastructure with Terraform
3. Clean up local kubeconfig
### Via Terraform Only
```bash
cd ~/hetzner-lab/terraform
terraform destroy -auto-approve
```
### Complete Cleanup
```bash
# Remove entire project
rm -rf ~/hetzner-lab
# Remove kubeconfig
rm ~/.kube/config-hetzner
# Remove from .bashrc
sed -i '/KUBECONFIG.*hetzner/d' ~/.bashrc
```
---
## 🔧 6. MANUAL INSTALLATION (If You Need Control)
### 6.1 Install Dependencies
```bash
# Update system
sudo apt-get update && sudo apt-get upgrade -y
# Install basic packages
sudo apt-get install -y gnupg software-properties-common curl wget unzip git jq python3-pip
# Install Terraform
wget -O- https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt-get update
sudo apt-get install -y terraform
# Install Ansible
sudo apt-get install -y ansible
# Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
rm kubectl
# Verify
terraform version
ansible --version
kubectl version --client
```
### 6.2 Create SSH Key
```bash
ssh-keygen -t rsa -b 4096 -N "" -f ~/.ssh/id_rsa
```
### 6.3 Deploy with Ansible
```bash
cd ~/hetzner-lab
# Configure Terraform
cd terraform
cp terraform.tfvars.example terraform.tfvars
nano terraform.tfvars # Add your token
# Deploy infrastructure
terraform init
terraform plan -out=tfplan
terraform apply tfplan
# Wait for servers to boot
sleep 60
# Test Ansible connectivity
cd ../ansible
ansible all -m ping
# Install K3s with Ansible
ansible-playbook playbooks/install-k3s.yml
# Use cluster
export KUBECONFIG=~/.kube/config-hetzner
kubectl get nodes
```
---
## 🔍 7. TROUBLESHOOTING
### Issue: Ansible Cannot Connect
```bash
# Check SSH keys are removed
cd ~/hetzner-lab/terraform
MASTER_IP=$(terraform output -raw master_ip)
ssh-keygen -R "$MASTER_IP"
# Test connection
ssh -o StrictHostKeyChecking=no root@$MASTER_IP "hostname"
# Retry Ansible
cd ../ansible
ansible all -m ping
```
### Issue: Workers Not Joining
```bash
# Check worker logs
cd ~/hetzner-lab/terraform
WORKER1_IP=$(terraform output -json worker_ips | jq -r '.[0]')
ssh root@$WORKER1_IP "journalctl -u k3s-agent -n 50"
# Reinstall K3s
cd ../scripts
./lab.sh reinstall
```
### Issue: kubectl Cannot Connect
```bash
# The kubeconfig is automatically updated by Ansible
# If still having issues, reinstall:
cd ~/hetzner-lab/scripts
./lab.sh reinstall
# Or manually fix kubeconfig:
cd ~/hetzner-lab/terraform
MASTER_IP=$(terraform output -raw master_ip)
sed -i "s/https:\/\/[0-9.]*:6443/https:\/\/$MASTER_IP:6443/g" ~/.kube/config-hetzner
```
### Issue: Terraform State Locked
```bash
cd ~/hetzner-lab/terraform
terraform force-unlock <LOCK_ID>
```
### Issue: K3s Installation Hangs
```bash
# Cancel the playbook (Ctrl+C)
# Check Ansible logs
cd ~/hetzner-lab/ansible
tail -f ansible.log
# Try with verbose mode
ansible-playbook playbooks/install-k3s.yml -vvv
```
---
## 📊 COST
|Configuration|Hourly|2 Hours|Day|Month|
|---|---|---|---|---|
|3x CX23|EUR 0.015|EUR 0.03|EUR 0.36|EUR 8.97|
---
## ✅ CHECKLIST
- [ ] Terraform installed
- [ ] Ansible installed
- [ ] kubectl installed
- [ ] SSH key created
- [ ] Hetzner API token obtained
- [ ] terraform.tfvars configured
- [ ] Infrastructure created
- [ ] Ansible connectivity tested
- [ ] K3s installed via Ansible
- [ ] kubectl configured automatically
- [ ] Cluster working
- [ ] **terraform destroy executed (don't forget!)**
---
## 🎯 QUICK REFERENCE
```bash
# Installation (one-time)
bash ~/install-hetzner-k3s.sh
# Create cluster (Terraform + Ansible)
cd ~/hetzner-lab/scripts && ./lab.sh start
# Use cluster
export KUBECONFIG=~/.kube/config-hetzner
kubectl get nodes
# Reinstall K3s only
./lab.sh reinstall
# Destroy cluster
./lab.sh stop
```
---
## 🌟 KEY IMPROVEMENTS
**Ansible Integration:**
- ✅ Automated K3s installation with retry logic
- ✅ Universal kubeconfig management
- ✅ Automatic IP updates in kubeconfig
- ✅ Clean cluster reset functionality
- ✅ SSH connectivity testing before deployment
**Operational Benefits:**
- ✅ No manual IP management required
- ✅ Repeatable deployments
- ✅ Easy troubleshooting with `reinstall` command
- ✅ Comprehensive error handling
- ✅ Ansible logs for debugging
---
**Done! You now have a fully automated K3s lab with Terraform + Ansible!** 🎉