diff --git a/.gitignore b/.gitignore index 44fa520..c3c7801 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,8 @@ *.swo *~ .DS_Store +*.tfvars +*.tfstate +*.tfstate.backup +.terraform/ +.terraform.lock.hcl diff --git a/terraform/cloud-init.yaml.tpl b/terraform/cloud-init.yaml.tpl deleted file mode 100644 index 39309fd..0000000 --- a/terraform/cloud-init.yaml.tpl +++ /dev/null @@ -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!" diff --git a/terraform/main.tf b/terraform/main.tf index b96ea7d..14b3ef5 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -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 } diff --git a/terraform/outputs.tf b/terraform/outputs.tf new file mode 100644 index 0000000..add1007 --- /dev/null +++ b/terraform/outputs.tf @@ -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 +} diff --git a/terraform/variables.tf b/terraform/variables.tf index 3a960a4..413e664 100644 --- a/terraform/variables.tf +++ b/terraform/variables.tf @@ -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 -}