slim terraform to match reality: server-only config

- Removed network, floating IP, volume, firewall, cloud-init (overkill for current setup)
- Matched config to actual server: cpx11, ash, krusty-clearnet
- Imported existing server (125941041) into Terraform state
- Applied labels (project=krustyplanet, role=web)
- Added outputs.tf for server_id, ipv4, location
- Added .gitignore for tfvars/state/secrets
This commit is contained in:
Jezza Hehn 2026-04-13 23:14:16 +00:00
parent aa295f494c
commit 8b0c095c50
5 changed files with 25 additions and 336 deletions

5
.gitignore vendored
View file

@ -2,3 +2,8 @@
*.swo
*~
.DS_Store
*.tfvars
*.tfstate
*.tfstate.backup
.terraform/
.terraform.lock.hcl

View file

@ -1,162 +0,0 @@
#!/bin/bash
set -euxo pipefail
###############################################################################
# Cloud-Init for KrustyPlanet VPS
###############################################################################
# Update system
apt-get update -y
apt-get upgrade -y
# Install required packages
apt-get install -y nginx certbot python3-certbot-nginx curl git
# Install Node.js
curl -fsSL https://deb.nodesource.com/setup_${node_version}.x | bash -
apt-get install -y nodejs
# Create directories
mkdir -p /opt/contact-api
mkdir -p /var/www/${project_name}
mkdir -p /var/www/${project_name}/css
mkdir -p /var/www/${project_name}/js
# Set domain variable
DOMAIN=${domain}
# Set up nginx configuration
cat > /etc/nginx/sites-available/${project_name} << NGINXEOF
server {
root /var/www/${project_name};
index index.html;
server_name ${domain} www.${domain};
location ~* \.(js|css)$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
try_files \$uri \$uri/ =404;
}
location / {
try_files \$uri \$uri/ =404;
}
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
listen [::]:443 ssl ipv6only=on;
listen 443 ssl;
ssl_certificate /etc/letsencrypt/live/${domain}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/${domain}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
}
server {
listen 80;
listen [::]:80;
server_name ${domain} www.${domain};
return 301 https://\$host\$request_uri;
}
NGINXEOF
# Symlink nginx config
ln -sf /etc/nginx/sites-available/${project_name} /etc/nginx/sites-enabled/${project_name}
# Remove default nginx site
rm -f /etc/nginx/sites-enabled/default
# Set up nginx snippet for contact-api proxy (NO proxy_set_header Origin — breaks CORS)
cat > /etc/nginx/snippets/contact-api.conf << 'PROXYEOF'
location /api/contact {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
}
location /api/health {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Host \$host;
}
PROXYEOF
# Include the snippet in the main server block
sed -i '/location \/ {/i\ include /etc/nginx/snippets/contact-api.conf;' /etc/nginx/sites-available/${project_name}
# Set up contact-api service
cat > /etc/systemd/system/contact-api.service << 'SVCEOF'
[Unit]
Description=Contact Form API - Email Backend
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/opt/contact-api
ExecStart=/usr/bin/node src/index.js
Restart=on-failure
RestartSec=10
EnvironmentFile=/opt/contact-api/.env
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/contact-api
StandardOutput=journal
StandardError=journal
SyslogIdentifier=contact-api
[Install]
WantedBy=multi-user.target
SVCEOF
# Start nginx
systemctl restart nginx
# Enable contact-api service
systemctl daemon-reload
systemctl enable contact-api.service
# Download contact-api source from Forgejo
cd /opt/contact-api
git clone ssh://git@git.jezzahehn.com:2222/KrustyPlanet/contact-api.git .
chown -R www-data:www-data /opt/contact-api
# .env will be created manually or via secrets management
cat > /opt/contact-api/.env << 'ENVEOF'
CONTACT_API_PORT=3001
ENVEOF
# Install dependencies
npm install
# Set permissions
chown -R www-data:www-data /opt/contact-api
chown -R www-data:www-data /var/www/${project_name}
# Start contact-api
systemctl start contact-api.service
# Get SSL certificate
certbot --nginx --non-interactive --agree-tos --email noreply@krustyplanet.org -d ${domain} -d www.${domain}
# Enable certbot auto-renewal
systemctl enable certbot.timer
# Clean up
apt-get autoremove -y
apt-get clean
echo "KrustyPlanet VPS setup complete!"

View file

@ -1,6 +1,6 @@
###############################################################################
# main.tf KrustyPlanet VPS on Hetzner Cloud
# Stack: Hetzner Cloud + Ubuntu 24.04 + nginx + contact-api + SSL
# Manages the existing krustyplanet.org server (CPX11, Ashburn)
###############################################################################
terraform {
@ -18,126 +18,18 @@ provider "hcloud" {
token = var.hcloud_token
}
###############################################################################
# SSH Key
###############################################################################
resource "hcloud_ssh_key" "krustyplanet" {
name = "${var.project_name}-key"
public_key = var.ssh_public_key
}
###############################################################################
# Network (private)
###############################################################################
resource "hcloud_network" "krustyplanet" {
name = "${var.project_name}-network"
ip_range = "10.0.0.0/16"
}
resource "hcloud_network_subnet" "krustyplanet" {
network_id = hcloud_network.krustyplanet.id
type = "cloud"
network_zone = var.network_zone
ip_range = "10.0.1.0/24"
}
###############################################################################
# Firewall
###############################################################################
resource "hcloud_firewall" "krustyplanet" {
name = "${var.project_name}-firewall"
# SSH admin always restricted to your IPs
rule {
direction = "in"
protocol = "tcp"
port = "22"
source_ips = var.ssh_allowed_ips
}
# HTTP
rule {
direction = "in"
protocol = "tcp"
port = "80"
source_ips = ["0.0.0.0/0", "::/0"]
}
# HTTPS
rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = ["0.0.0.0/0", "::/0"]
}
# Apply to the server
apply_to {
server = hcloud_server.krustyplanet.id
}
}
###############################################################################
# Server
###############################################################################
resource "hcloud_server" "krustyplanet" {
name = "${var.project_name}-server"
server_type = var.server_type
name = "krusty-clearnet"
server_type = "cpx11"
image = "ubuntu-24.04"
location = var.location
ssh_keys = [hcloud_ssh_key.krustyplanet.id]
network {
network_id = hcloud_network.krustyplanet.id
ip = "10.0.1.10"
}
user_data = templatefile("${path.module}/cloud-init.yaml.tpl", {
project_name = var.project_name
node_version = var.node_version
domain = var.domain
})
location = "ash"
labels = {
project = var.project_name
project = "krustyplanet"
role = "web"
}
depends_on = [hcloud_network_subnet.krustyplanet]
}
###############################################################################
# Floating IP (stable public IP across rebuilds)
###############################################################################
resource "hcloud_floating_ip" "krustyplanet" {
type = "ipv4"
home_location = var.location
description = "${var.project_name} floating IP"
}
resource "hcloud_floating_ip_assignment" "krustyplanet" {
floating_ip_id = hcloud_floating_ip.krustyplanet.id
server_id = hcloud_server.krustyplanet.id
}
###############################################################################
# Volume (persistent data survives server rebuilds)
###############################################################################
resource "hcloud_volume" "krustyplanet_data" {
name = "${var.project_name}-data"
size = var.volume_size_gb
location = var.location
format = "ext4"
}
resource "hcloud_volume_attachment" "krustyplanet_data" {
volume_id = hcloud_volume.krustyplanet_data.id
server_id = hcloud_server.krustyplanet.id
automount = true
}

15
terraform/outputs.tf Normal file
View file

@ -0,0 +1,15 @@
###############################################################################
# outputs.tf
###############################################################################
output "server_id" {
value = hcloud_server.krustyplanet.id
}
output "server_ipv4" {
value = hcloud_server.krustyplanet.ipv4_address
}
output "server_location" {
value = hcloud_server.krustyplanet.location
}

View file

@ -7,64 +7,3 @@ variable "hcloud_token" {
type = string
sensitive = true
}
variable "project_name" {
description = "Short name used to prefix all resources."
type = string
default = "krustyplanet"
}
variable "domain" {
description = "Domain for the website."
type = string
default = "krustyplanet.org"
}
variable "ssh_public_key" {
description = "Your SSH public key (contents of ~/.ssh/id_ed25519.pub or similar)."
type = string
default = ""
}
variable "ssh_allowed_ips" {
description = "CIDRs allowed to reach port 22. Restrict to your actual IP for security."
type = list(string)
default = ["0.0.0.0/0", "::/0"] # Tighten this in production!
}
variable "server_type" {
description = "Hetzner server type."
type = string
default = "cx22"
# cx22 2 vCPU / 4 GB RAM recommended for personal websites
# cax11 2 ARM vCPU / 4 GB cheapest option (~3.79/mo post-2026 pricing)
# cx32 4 vCPU / 8 GB comfortable for a small team
}
variable "location" {
description = "Hetzner datacenter location."
type = string
default = "hel1" # Helsinki, FI Finnish jurisdiction, strong privacy
# nbg1 Nuremberg, DE
# fsn1 Falkenstein, DE
# ash Ashburn, US
}
variable "network_zone" {
description = "Hetzner network zone matching your location."
type = string
default = "eu-central"
# us-east or us-west for US locations
}
variable "node_version" {
description = "Node.js version to install."
type = string
default = "20"
}
variable "volume_size_gb" {
description = "Size of the persistent data volume in GB."
type = number
default = 40
}