114 lines
3.3 KiB
Terraform
114 lines
3.3 KiB
Terraform
# aibook-fun - Main Infrastructure
|
|
# Managed by OpenTofu
|
|
|
|
resource "digitalocean_droplet" "web" {
|
|
name = "aibook-fun"
|
|
image = var.droplet_image
|
|
size = var.droplet_size
|
|
region = var.region
|
|
monitoring = true
|
|
backups = true
|
|
|
|
# CPU/RAM-only resize (provider default is true = also grow the disk).
|
|
resize_disk = false
|
|
|
|
ssh_keys = var.ssh_key_fingerprints
|
|
|
|
user_data = templatefile("${path.module}/templates/cloud-init.yaml", {
|
|
project_name = "aibook_fun"
|
|
})
|
|
|
|
tags = ["aibook-fun", "web", "weown-ai"]
|
|
|
|
lifecycle {
|
|
# A droplet destroy means losing every Docker volume + cert state on it.
|
|
# make any such plan a hard error instead of an accident.
|
|
prevent_destroy = true
|
|
# user_data/tags/ssh_keys are create-time-only on DO; ignore drift so
|
|
# unrelated changes don't arm `# forces replacement` on next apply.
|
|
ignore_changes = [user_data, tags, ssh_keys]
|
|
}
|
|
}
|
|
|
|
# Optional encrypted-at-rest data volume (default 0 = stateless: droplet local
|
|
# disk only). Enable by setting data_volume_size_gb > 0 if the site ever needs
|
|
# durable state; DO block storage is encrypted at rest, the droplet disk is not.
|
|
resource "digitalocean_volume" "data" {
|
|
count = var.data_volume_size_gb > 0 ? 1 : 0
|
|
region = var.region
|
|
name = "aibook-fun-data"
|
|
size = var.data_volume_size_gb
|
|
initial_filesystem_type = "ext4"
|
|
description = "aibook-fun encrypted-at-rest data (docker data-root)"
|
|
|
|
lifecycle {
|
|
prevent_destroy = true
|
|
}
|
|
}
|
|
|
|
resource "digitalocean_volume_attachment" "data" {
|
|
count = var.data_volume_size_gb > 0 ? 1 : 0
|
|
droplet_id = digitalocean_droplet.web.id
|
|
volume_id = digitalocean_volume.data[0].id
|
|
}
|
|
|
|
resource "digitalocean_reserved_ip" "web" {
|
|
region = var.region
|
|
}
|
|
|
|
resource "digitalocean_reserved_ip_assignment" "web" {
|
|
ip_address = digitalocean_reserved_ip.web.ip_address
|
|
droplet_id = digitalocean_droplet.web.id
|
|
}
|
|
|
|
#trivy:ignore:AVD-DIG-0001 # Public web server: HTTP/HTTPS inbound from internet is required by design
|
|
#trivy:ignore:AVD-DIG-0003 # Public web server: unrestricted outbound required for package installs, ACME, image pulls
|
|
resource "digitalocean_firewall" "web" {
|
|
name = "aibook-fun-fw"
|
|
droplet_ids = [digitalocean_droplet.web.id]
|
|
|
|
# SSH — restrict via var.ssh_source_cidrs. ⚠️ PRODUCTION: pin to admin IP/32 or VPN
|
|
inbound_rule {
|
|
protocol = "tcp"
|
|
port_range = "22"
|
|
source_addresses = var.ssh_source_cidrs
|
|
}
|
|
|
|
# HTTP (ACME challenge + redirect)
|
|
inbound_rule {
|
|
protocol = "tcp"
|
|
port_range = "80"
|
|
source_addresses = ["0.0.0.0/0", "::/0"]
|
|
}
|
|
|
|
# HTTPS
|
|
inbound_rule {
|
|
protocol = "tcp"
|
|
port_range = "443"
|
|
source_addresses = ["0.0.0.0/0", "::/0"]
|
|
}
|
|
|
|
# HTTPS/QUIC (HTTP/3)
|
|
inbound_rule {
|
|
protocol = "udp"
|
|
port_range = "443"
|
|
source_addresses = ["0.0.0.0/0", "::/0"]
|
|
}
|
|
|
|
# All outbound TCP (apt, ACME, container image pulls)
|
|
outbound_rule {
|
|
protocol = "tcp"
|
|
port_range = "1-65535"
|
|
destination_addresses = ["0.0.0.0/0", "::/0"]
|
|
}
|
|
|
|
# All outbound UDP (DNS, NTP, QUIC)
|
|
outbound_rule {
|
|
protocol = "udp"
|
|
port_range = "1-65535"
|
|
destination_addresses = ["0.0.0.0/0", "::/0"]
|
|
}
|
|
|
|
tags = ["aibook-fun"]
|
|
}
|