Mirrors the prod deploy-k3s/ setup but runs all services in-cluster on a single node: PostgreSQL (replaces Neon), MinIO S3-compatible storage (replaces B2), Redis, API, worker, and admin. Includes fully automated setup scripts (00-init through 04-verify), server hardening (SSH, fail2ban, ufw), Let's Encrypt TLS via Traefik, network policies, RBAC, and security contexts matching prod. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
125 lines
3.8 KiB
Bash
Executable File
125 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
# shellcheck source=_config.sh
|
|
source "${SCRIPT_DIR}/_config.sh"
|
|
|
|
log() { printf '[provision] %s\n' "$*"; }
|
|
die() { printf '[provision][error] %s\n' "$*" >&2; exit 1; }
|
|
|
|
# --- Prerequisites ---
|
|
|
|
command -v hetzner-k3s >/dev/null 2>&1 || die "Missing: hetzner-k3s CLI. Install: https://github.com/vitobotta/hetzner-k3s"
|
|
command -v kubectl >/dev/null 2>&1 || die "Missing: kubectl"
|
|
|
|
HCLOUD_TOKEN="$(cfg_require cluster.hcloud_token "Hetzner API token")"
|
|
export HCLOUD_TOKEN
|
|
|
|
# Validate SSH keys
|
|
SSH_PUB="$(cfg cluster.ssh_public_key | sed "s|~|${HOME}|g")"
|
|
SSH_PRIV="$(cfg cluster.ssh_private_key | sed "s|~|${HOME}|g")"
|
|
[[ -f "${SSH_PUB}" ]] || die "SSH public key not found: ${SSH_PUB}"
|
|
[[ -f "${SSH_PRIV}" ]] || die "SSH private key not found: ${SSH_PRIV}"
|
|
|
|
# --- Generate hetzner-k3s cluster config from config.yaml ---
|
|
|
|
CLUSTER_CONFIG="${DEPLOY_DIR}/cluster-config.yaml"
|
|
log "Generating cluster-config.yaml from config.yaml..."
|
|
generate_cluster_config > "${CLUSTER_CONFIG}"
|
|
|
|
# --- Provision ---
|
|
|
|
INSTANCE_TYPE="$(cfg cluster.instance_type)"
|
|
LOCATION="$(cfg cluster.location)"
|
|
NODE_COUNT="$(node_count)"
|
|
|
|
log "Provisioning K3s cluster on Hetzner Cloud..."
|
|
log " Nodes: ${NODE_COUNT}x ${INSTANCE_TYPE} in ${LOCATION}"
|
|
log " This takes about 5-10 minutes."
|
|
echo ""
|
|
|
|
hetzner-k3s create --config "${CLUSTER_CONFIG}"
|
|
|
|
KUBECONFIG_PATH="${DEPLOY_DIR}/kubeconfig"
|
|
|
|
if [[ ! -f "${KUBECONFIG_PATH}" ]]; then
|
|
die "Provisioning completed but kubeconfig not found. Check hetzner-k3s output."
|
|
fi
|
|
|
|
# --- Write node IPs back to config.yaml ---
|
|
|
|
log "Querying node IPs..."
|
|
export KUBECONFIG="${KUBECONFIG_PATH}"
|
|
|
|
python3 -c "
|
|
import yaml, subprocess, json
|
|
|
|
# Get node info from kubectl
|
|
result = subprocess.run(
|
|
['kubectl', 'get', 'nodes', '-o', 'json'],
|
|
capture_output=True, text=True
|
|
)
|
|
nodes_json = json.loads(result.stdout)
|
|
|
|
# Build name → IP map
|
|
ip_map = {}
|
|
for node in nodes_json.get('items', []):
|
|
name = node['metadata']['name']
|
|
for addr in node.get('status', {}).get('addresses', []):
|
|
if addr['type'] == 'ExternalIP':
|
|
ip_map[name] = addr['address']
|
|
break
|
|
else:
|
|
for addr in node.get('status', {}).get('addresses', []):
|
|
if addr['type'] == 'InternalIP':
|
|
ip_map[name] = addr['address']
|
|
break
|
|
|
|
# Update config.yaml with IPs
|
|
with open('${CONFIG_FILE}') as f:
|
|
config = yaml.safe_load(f)
|
|
|
|
updated = 0
|
|
for i, node in enumerate(config.get('nodes', [])):
|
|
for real_name, ip in ip_map.items():
|
|
if node['name'] in real_name or real_name in node['name']:
|
|
config['nodes'][i]['ip'] = ip
|
|
config['nodes'][i]['name'] = real_name
|
|
updated += 1
|
|
break
|
|
|
|
if updated == 0 and ip_map:
|
|
# Names didn't match — assign by index
|
|
for i, (name, ip) in enumerate(sorted(ip_map.items())):
|
|
if i < len(config['nodes']):
|
|
config['nodes'][i]['name'] = name
|
|
config['nodes'][i]['ip'] = ip
|
|
updated += 1
|
|
|
|
with open('${CONFIG_FILE}', 'w') as f:
|
|
yaml.dump(config, f, default_flow_style=False, sort_keys=False)
|
|
|
|
print(f'Updated {updated} node IPs in config.yaml')
|
|
for name, ip in sorted(ip_map.items()):
|
|
print(f' {name}: {ip}')
|
|
"
|
|
|
|
# --- Label Redis node ---
|
|
|
|
REDIS_NODE="$(nodes_with_role redis | head -1)"
|
|
if [[ -n "${REDIS_NODE}" ]]; then
|
|
# Find the actual K8s node name that matches
|
|
ACTUAL_NODE="$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}' | tr ' ' '\n' | head -1)"
|
|
log "Labeling node ${ACTUAL_NODE} for Redis..."
|
|
kubectl label node "${ACTUAL_NODE}" honeydue/redis=true --overwrite
|
|
fi
|
|
|
|
log ""
|
|
log "Cluster provisioned successfully."
|
|
log ""
|
|
log "Next steps:"
|
|
log " export KUBECONFIG=${KUBECONFIG_PATH}"
|
|
log " kubectl get nodes"
|
|
log " ./scripts/02-setup-secrets.sh"
|