root 74eeabb354
Some checks failed
PR Checks / tofu-checks (pull_request) Failing after 2s
1/1 projects applied successfully.
feat: add tenant VM module for VM-as-a-Service (Step 5.2)
Reusable OpenTofu module for creating isolated tenant VMs with:
- Public IP on vmbr1 (bridged, firewall=true)
- Cloud-init: password auth, fail2ban, UFW hardening
- Per-VM Proxmox firewall (IN: SSH+ICMP, OUT: allow, block SMTP)

Includes test-tenant VM (185.47.204.227) for verification.

Changes:
- modules/tenant-vm/ — reusable module (VM + FW + cloud-init)
- environments/production/tenant-vms.tf — tenant VM definitions
- policies/security.rego — require firewall=true on vmbr1
- atlantis.yaml — trigger on module file changes
- main.tf — updated host prerequisites comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-11 20:01:38 +01:00

136 lines
4.2 KiB
HCL

# Production environment — managed by Claude AI via Atlantis
# Changes to this file go through PR → plan → approve → apply
terraform {
required_version = ">= 1.6.0"
backend "s3" {
bucket = "tofu-state"
key = "production/terraform.tfstate"
endpoints = { s3 = "http://minio:9000" }
region = "us-east-1"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_requesting_account_id = true
use_path_style = true
}
required_providers {
proxmox = {
source = "bpg/proxmox"
version = "~> 0.90"
}
}
}
provider "proxmox" {
endpoint = "https://185.47.204.226:8006/"
insecure = true # self-signed cert
# api_token read from PROXMOX_VE_API_TOKEN env var
# Decrypted from SOPS by Atlantis custom workflow
ssh {
agent = false
username = "root"
private_key = file("/secrets/ssh-key")
}
}
# Verify Proxmox connectivity — read cluster nodes
data "proxmox_virtual_environment_nodes" "nodes" {}
output "proxmox_nodes" {
description = "Proxmox cluster node names"
value = data.proxmox_virtual_environment_nodes.nodes.names
}
# ─── Cloud Images ─────────────────────────────────────────────────────────────
# Managed by OpenTofu — no manual wget needed
resource "proxmox_virtual_environment_download_file" "ubuntu_2404_cloud" {
content_type = "iso"
datastore_id = "local"
node_name = "georgeops"
url = "https://cloud-images.ubuntu.com/releases/24.04/release/ubuntu-24.04-server-cloudimg-amd64.img"
file_name = "ubuntu-24.04-cloudimg-amd64.img"
overwrite_unmanaged = true # adopt existing manually-downloaded file
}
# ─── Host Prerequisites (not manageable via Proxmox API) ─────────────────────
# vmbr1 bridge: 185.47.204.226/28, bridge-ports eth0 (public IP + tenant VMs)
# vmbr0 bridge: 10.10.10.1/24, bridge-ports none (NAT for internal VMs)
# NAT: iptables MASQUERADE 10.10.10.0/24 → vmbr1 (post-up)
# Host protect: iptables DROP .227-.236 → host INPUT (post-up on vmbr1)
# ip_forward: /etc/sysctl.d/99-ip-forward.conf (net.ipv4.ip_forward = 1)
# Snippets: pvesm set local --content iso,vztmpl,backup,snippets
# Reason: Proxmox API does not support post-up/post-down (bpg/proxmox #1454)
# See: proxmox-patterns.md in Claude memory
# ─── Test VM (Step 4.6) ──────────────────────────────────────────────────────
resource "proxmox_virtual_environment_vm" "test_vm_01" {
depends_on = [proxmox_virtual_environment_download_file.ubuntu_2404_cloud]
name = "test-vm-01"
node_name = "georgeops"
vm_id = 100
tags = ["test", "tofu", "ubuntu"]
stop_on_destroy = true
started = true
on_boot = false # test VM, no auto-start on host reboot
cpu {
cores = 2
type = "x86-64-v2-AES"
}
memory {
dedicated = 2048
}
disk {
datastore_id = "local"
# NOTE: Using hardcoded path (not resource reference) because file_id forces VM replacement.
# The download_file resource above ensures the image exists via depends_on.
file_id = "local:iso/ubuntu-24.04-cloudimg-amd64.img"
interface = "virtio0"
size = 20
file_format = "qcow2"
discard = "on"
iothread = true
}
network_device {
bridge = "vmbr0"
firewall = false # NAT bridge — isolation via NAT + host firewall
}
initialization {
datastore_id = "local"
ip_config {
ipv4 {
address = "10.10.10.100/24"
gateway = "10.10.10.1"
}
}
dns {
servers = ["8.8.8.8", "1.1.1.1"]
}
user_account {
username = "ubuntu"
keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDO+Y8ns0RgUfR21POlIVsHD+Lp+x7cUBupqXsyMeVNZ claude@control-plane"]
}
}
}
output "test_vm_01_ip" {
description = "Test VM IP address (NAT behind bare_srv_1)"
value = "10.10.10.100"
}