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:
parent
aa295f494c
commit
8b0c095c50
5 changed files with 25 additions and 336 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -2,3 +2,8 @@
|
||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
*.tfvars
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.backup
|
||||||
|
.terraform/
|
||||||
|
.terraform.lock.hcl
|
||||||
|
|
|
||||||
|
|
@ -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!"
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
###############################################################################
|
###############################################################################
|
||||||
# main.tf — KrustyPlanet VPS on Hetzner Cloud
|
# 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 {
|
terraform {
|
||||||
|
|
@ -18,126 +18,18 @@ provider "hcloud" {
|
||||||
token = var.hcloud_token
|
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
|
# Server
|
||||||
###############################################################################
|
###############################################################################
|
||||||
|
|
||||||
resource "hcloud_server" "krustyplanet" {
|
resource "hcloud_server" "krustyplanet" {
|
||||||
name = "${var.project_name}-server"
|
name = "krusty-clearnet"
|
||||||
server_type = var.server_type
|
server_type = "cpx11"
|
||||||
image = "ubuntu-24.04"
|
image = "ubuntu-24.04"
|
||||||
location = var.location
|
location = "ash"
|
||||||
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
|
|
||||||
})
|
|
||||||
|
|
||||||
labels = {
|
labels = {
|
||||||
project = var.project_name
|
project = "krustyplanet"
|
||||||
role = "web"
|
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
15
terraform/outputs.tf
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -7,64 +7,3 @@ variable "hcloud_token" {
|
||||||
type = string
|
type = string
|
||||||
sensitive = true
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue