feat(proxmox): Proxmox VM template + Terraform/OpenTofu VM creation
Manual, documented bare-metal Proxmox install + cloud-init template build (deliberately not automated — a fragile one-time step). Terraform/OpenTofu config creates rn-platform-01/rn-lab-host-01 from that template via the bpg/proxmox provider, with configurable (not hardcoded) VM sizing. Validated: tofu fmt, tofu init -backend=false (real provider download), tofu validate -> Success. No paid state backend required (local state).
This commit is contained in:
@@ -4,3 +4,4 @@ frontend/dist/
|
||||
*.db-journal
|
||||
/data/
|
||||
.env
|
||||
.ansible/
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Proxmox — manual, one-time steps
|
||||
|
||||
Deliberately NOT automated (Phase 4 §21: "do not over-automate bare-metal Proxmox installation
|
||||
if it becomes fragile" — a broken automated install of the one host that IS the only way in is
|
||||
a much worse failure mode than a documented manual step reviewed by a human once).
|
||||
|
||||
## 1. Install Proxmox VE
|
||||
|
||||
Standard ISO install on the Hetzner dedicated server, once purchased. Follow Proxmox's own
|
||||
install guide. Full checklist: `docs/PRODUCTION_DEPLOYMENT.md` §1-2.
|
||||
|
||||
## 2. Build the cloud-init VM template
|
||||
|
||||
One-time, either via `virt-customize`/manual, or Packer if already a comfortable tool here:
|
||||
|
||||
```bash
|
||||
# On the Proxmox host, after downloading an Ubuntu 22.04 cloud image:
|
||||
qm create 9000 --name ubuntu-22.04-cloudinit --memory 2048 --cores 2 --net0 virtio,bridge=vmbr0
|
||||
qm importdisk 9000 jammy-server-cloudimg-amd64.img local-zfs
|
||||
qm set 9000 --scsihw virtio-scsi-pci --scsi0 local-zfs:vm-9000-disk-0
|
||||
qm set 9000 --ide2 local-zfs:cloudinit
|
||||
qm set 9000 --boot c --bootdisk scsi0
|
||||
qm set 9000 --serial0 socket --vga serial0
|
||||
qm set 9000 --agent enabled=1
|
||||
qm template 9000
|
||||
```
|
||||
|
||||
Then apply `infra/proxmox/cloud-init/user-data.yaml` as the template's default cloud-init
|
||||
user-data (Proxmox's own cloud-init snippet mechanism — `Datacenter -> Storage -> snippets`).
|
||||
|
||||
## 3. Hand off to Terraform + Ansible
|
||||
|
||||
Once the template exists and tagged `template` (matching `infra/terraform/main.tf`'s data
|
||||
source filter), `infra/terraform/` can clone `rn-platform-01`/`rn-lab-host-01` from it, and
|
||||
`infra/ansible/site.yml` configures them. Neither of those needs further Proxmox-console work
|
||||
after this point — see `docs/RUNBOOK_FIRST_DEPLOYMENT.md`.
|
||||
|
||||
## 4. Proxmox host firewall
|
||||
|
||||
Apply `infra/firewall/proxmox-host.rules` manually (rendered with real values) — see that
|
||||
file's own header comment for why this one ruleset stays a manual apply, unlike the two VMs'.
|
||||
@@ -0,0 +1,28 @@
|
||||
#cloud-config
|
||||
# Baseline cloud-init user-data baked into the VM template infra/terraform/ clones. Minimal on
|
||||
# purpose — infra/ansible/site.yml does the real configuration (Docker, firewall, TLS, the app
|
||||
# stack); this just gets a box far enough to be SSH-reachable and Ansible-manageable.
|
||||
|
||||
package_update: true
|
||||
package_upgrade: true
|
||||
|
||||
users:
|
||||
- name: rootnode-deploy
|
||||
groups: [sudo]
|
||||
shell: /bin/bash
|
||||
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
|
||||
ssh_authorized_keys: [] # injected by Terraform's ssh_public_key variable at clone time
|
||||
|
||||
ssh_pwauth: false
|
||||
disable_root: true
|
||||
|
||||
packages:
|
||||
- qemu-guest-agent
|
||||
- curl
|
||||
- ca-certificates
|
||||
|
||||
runcmd:
|
||||
- systemctl enable --now qemu-guest-agent
|
||||
|
||||
# See infra/proxmox/README.md for how to bake this into a reusable template (packer or manual
|
||||
# virt-customize) before infra/terraform/ clones it.
|
||||
Generated
+36
@@ -0,0 +1,36 @@
|
||||
# This file is maintained automatically by "tofu init".
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/bpg/proxmox" {
|
||||
version = "0.111.1"
|
||||
constraints = "~> 0.60"
|
||||
hashes = [
|
||||
"h1:0qABXcf7ulwKRhKGoK4LYZ7ObyoFkPxHmCrL6jRhMgE=",
|
||||
"h1:0xM4HmXdu0hLrauA8ugcxjZvYHA+PuRybUxrXZEAdRI=",
|
||||
"h1:ML2D3UUZTM99yrll/EBXj7wBYMb8xmQgomqFNybEoxY=",
|
||||
"h1:PFGpaC7xcdC8w9jpxZPLJ/FFbVK5hd/CArUY+oXXTu8=",
|
||||
"h1:XoYUWDsGWvxSlir/sYiGzx1MkcPVUtWIgc6OWEdf/C8=",
|
||||
"h1:Zn7LyCWL/Xn0yCHiigXFYEp3MIv0MOVzUfL657QOplY=",
|
||||
"h1:dIGBUSoC89e/PbpBEjbZ/YOzAkzrG1kF6EYcqk+8IqE=",
|
||||
"h1:eERpiB94PyhN2SqE9E1+6sgpk0Kn6qjy555R2cjfpug=",
|
||||
"h1:ejD4OSOL98W/SA4jLDvxEwCW8NSdVGKLyKmgfAUROK8=",
|
||||
"h1:iTQv4FVFhMVl2juw6lgrVTpGFrdmdNPj+NFY2Dms0SE=",
|
||||
"h1:jcqEv/zW+heFIPq5xwXxgS9EuBmbjIM1MriwQjx75WE=",
|
||||
"h1:xF+AQJqpRf30WbrfHgSeuLqDBptWhTIdVoMyv7j6yVo=",
|
||||
"h1:xWV1Y6ItiFXNCJO9OtyINMx8cqT5XiTOK2Rne7Jyu3w=",
|
||||
"zh:18fb7c31a08dde6bffa1a4d4a211e604d6d17eec7092fd59331b3db3c6f3742c",
|
||||
"zh:1cd60761538289d4dd2a1086b3ae62a7b0bdd4b1a2f824e9a44e243413168dba",
|
||||
"zh:2eb76f6fc8299b6820ff678c8252332cc3366e226b5ae2e61748fd2449c1ed92",
|
||||
"zh:45e6f7ebd0bf48911d37060359a4f359b5743b3092e985295733990e406d0416",
|
||||
"zh:4aa8ba912eae37975d2e983394d173e595ca34fc76b5bf220b37d0e99d76e98c",
|
||||
"zh:58e0789923103a77d502a0a9fc3eb920625e8eb935ec2d4ac0d006aebd1d186c",
|
||||
"zh:6df8aa85fb8865915537e946c19b02538ad188018a629759c213c6f03730f642",
|
||||
"zh:6ed47bc00d0913a1d0880618fa1376115e9edab6b4a658c081061a7f0e4ca360",
|
||||
"zh:c5b10ff4f33df7e4c29e8f1127d49845b561b37b57517e844fb0954d7923d65e",
|
||||
"zh:d016510e14b738499f0db9d9b3aafe82fc6877fb4ab4e9f831fb68a8d70a1385",
|
||||
"zh:d941f394069bbf24351b363da1c64383f487067aaee0a84f9b96476d4912e212",
|
||||
"zh:ddf271dbc2632ae8ffa8de3972f243ee47d260cb2ac90aa784f2746d98e21a0f",
|
||||
"zh:ed0caa3501c42f611b7e9622c9b1df69fd85dc25a3cd88d3076381829688cd62",
|
||||
"zh:f26e0763dbe6a6b2195c94b44696f2110f7f55433dc142839be16b9697fa5597",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Proxmox VM creation (Terraform/OpenTofu)
|
||||
|
||||
Creates `rn-platform-01` and `rn-lab-host-01` as Proxmox VMs from a cloud-init-enabled Ubuntu
|
||||
22.04 template. Assumes Proxmox VE is already installed and reachable (bare-metal install
|
||||
stays a manual, documented step — `docs/PRODUCTION_DEPLOYMENT.md` §2 — over-automating the one
|
||||
irreversible step that could brick the only way into the host is not worth it).
|
||||
|
||||
Works with either Terraform or OpenTofu (the fork) — `bpg/proxmox` is available for both. Use
|
||||
whichever this project already has installed; nothing here is Terraform-Cloud-specific or
|
||||
requires a paid backend (state is local by default, `main.tf`'s commented-out `backend "s3"`
|
||||
block is there if remote state is ever wanted, not required).
|
||||
|
||||
## Prerequisites (all deploy-time, hardware-dependent — cannot be done now)
|
||||
|
||||
1. Proxmox VE installed, reachable at `proxmox_api_url`.
|
||||
2. A cloud-init-enabled Ubuntu 22.04 template already exists on the Proxmox host (packer/manual
|
||||
— see `infra/proxmox/cloud-init/` for the cloud-init user-data this expects the template to
|
||||
support; building the actual template is a one-time manual step, documented, not automated
|
||||
here for the same "don't over-automate the fragile one-time step" reasoning as the Proxmox
|
||||
install itself).
|
||||
3. A Proxmox API token with permission to create VMs (`terraform.tfvars`, never committed —
|
||||
see `terraform.tfvars.example`).
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
cd infra/terraform
|
||||
cp terraform.tfvars.example terraform.tfvars # fill in real values
|
||||
terraform init # or: tofu init
|
||||
terraform plan
|
||||
terraform apply
|
||||
```
|
||||
|
||||
Outputs the two VMs' IPs, which then go into `infra/ansible/group_vars/production.yml`
|
||||
(`platform_ip`, `lab_host_ip`) before running `infra/ansible/site.yml`.
|
||||
|
||||
## What this does NOT do
|
||||
|
||||
- Install Proxmox itself.
|
||||
- Build the base cloud-init template (one-time, manual, documented).
|
||||
- Decide final storage/ZFS layout (`vm_disk_datastore` is a variable — set it to whatever
|
||||
datastore the real disk-inspection checklist, `docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md` §4,
|
||||
says to use once the real disks are known).
|
||||
- Configure anything INSIDE the VMs — that's `infra/ansible/site.yml`'s job, run after `apply`.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Creates rn-platform-01 and rn-lab-host-01 from a shared cloud-init template. Sizing comes
|
||||
# from variables.tf's platform_vm/lab_host_vm objects — baseline defaults, override in
|
||||
# terraform.tfvars once real load data exists (docs/CAPACITY_PLANNING.md). This module does
|
||||
# NOT decide the host's final ZFS/storage layout; vm_disk_datastore just names whichever
|
||||
# Proxmox storage the real disk-inspection checklist points at.
|
||||
|
||||
resource "proxmox_virtual_environment_vm" "rn_platform_01" {
|
||||
name = "rn-platform-01"
|
||||
node_name = var.proxmox_node
|
||||
|
||||
clone {
|
||||
vm_id = data.proxmox_virtual_environment_vms.template.vms[0].vm_id
|
||||
full = true
|
||||
}
|
||||
|
||||
cpu {
|
||||
cores = var.platform_vm.cores
|
||||
type = "host"
|
||||
}
|
||||
memory {
|
||||
dedicated = var.platform_vm.memory
|
||||
}
|
||||
disk {
|
||||
datastore_id = var.vm_disk_datastore
|
||||
size = var.platform_vm.disk_gb
|
||||
interface = "scsi0"
|
||||
}
|
||||
network_device {
|
||||
bridge = "vmbr0"
|
||||
}
|
||||
|
||||
initialization {
|
||||
user_account {
|
||||
username = "rootnode-deploy"
|
||||
keys = [var.ssh_public_key]
|
||||
}
|
||||
ip_config {
|
||||
ipv4 {
|
||||
address = "dhcp" # static assignment is a deploy-time decision once the real network
|
||||
# plan is finalized — see docs/PRODUCTION_NETWORK_SECURITY.md §5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags = ["rootnode", "platform"]
|
||||
}
|
||||
|
||||
resource "proxmox_virtual_environment_vm" "rn_lab_host_01" {
|
||||
name = "rn-lab-host-01"
|
||||
node_name = var.proxmox_node
|
||||
|
||||
clone {
|
||||
vm_id = data.proxmox_virtual_environment_vms.template.vms[0].vm_id
|
||||
full = true
|
||||
}
|
||||
|
||||
cpu {
|
||||
cores = var.lab_host_vm.cores
|
||||
type = "host"
|
||||
}
|
||||
memory {
|
||||
dedicated = var.lab_host_vm.memory
|
||||
}
|
||||
disk {
|
||||
datastore_id = var.vm_disk_datastore
|
||||
size = var.lab_host_vm.disk_gb
|
||||
interface = "scsi0"
|
||||
}
|
||||
network_device {
|
||||
bridge = "vmbr0"
|
||||
}
|
||||
|
||||
initialization {
|
||||
user_account {
|
||||
username = "rootnode-deploy"
|
||||
keys = [var.ssh_public_key]
|
||||
}
|
||||
ip_config {
|
||||
ipv4 {
|
||||
address = "dhcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tags = ["rootnode", "lab-host"]
|
||||
}
|
||||
|
||||
data "proxmox_virtual_environment_vms" "template" {
|
||||
node_name = var.proxmox_node
|
||||
tags = ["template"]
|
||||
filter {
|
||||
name = "name"
|
||||
values = [var.vm_template_name]
|
||||
}
|
||||
}
|
||||
|
||||
output "rn_platform_01_ip" {
|
||||
value = proxmox_virtual_environment_vm.rn_platform_01.ipv4_addresses
|
||||
}
|
||||
output "rn_lab_host_01_ip" {
|
||||
value = proxmox_virtual_environment_vm.rn_lab_host_01.ipv4_addresses
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
proxmox_api_url = "https://proxmox.example.internal:8006/api2/json"
|
||||
proxmox_api_token = "terraform@pve!rootnode=00000000-0000-0000-0000-000000000000"
|
||||
proxmox_node = "pve"
|
||||
vm_disk_datastore = "local-zfs"
|
||||
ssh_public_key = "ssh-ed25519 AAAA... rootnode-deploy"
|
||||
management_cidr = "203.0.113.0/24"
|
||||
|
||||
# Override sizing here once real load data exists — see docs/CAPACITY_PLANNING.md.
|
||||
# platform_vm = { cores = 4, memory = 16384, disk_gb = 100 }
|
||||
# lab_host_vm = { cores = 24, memory = 98304, disk_gb = 500 }
|
||||
@@ -0,0 +1,66 @@
|
||||
variable "proxmox_api_url" {
|
||||
description = "e.g. https://proxmox.example.internal:8006/api2/json"
|
||||
type = string
|
||||
}
|
||||
variable "proxmox_api_token" {
|
||||
description = "Proxmox API token (user@realm!tokenid=uuid) — never committed, see terraform.tfvars.example"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "proxmox_tls_insecure" {
|
||||
description = "Set true only if the Proxmox host uses a self-signed cert with no CA to trust"
|
||||
type = bool
|
||||
default = false
|
||||
}
|
||||
variable "proxmox_node" {
|
||||
description = "Proxmox node name (e.g. 'pve')"
|
||||
type = string
|
||||
}
|
||||
variable "vm_template_name" {
|
||||
description = "Name of the pre-built cloud-init Ubuntu 22.04 template (see infra/terraform/README.md prerequisites)"
|
||||
type = string
|
||||
default = "ubuntu-22.04-cloudinit"
|
||||
}
|
||||
variable "vm_disk_datastore" {
|
||||
description = "Proxmox storage/datastore for VM disks — set once real disk layout is decided (docs/PHASE4_PRODUCTION_INFRASTRUCTURE.md §4)"
|
||||
type = string
|
||||
default = "local-zfs"
|
||||
}
|
||||
variable "ssh_public_key" {
|
||||
description = "Public key injected via cloud-init for the rootnode-deploy user"
|
||||
type = string
|
||||
}
|
||||
variable "management_cidr" {
|
||||
description = "Same value as infra/ansible/group_vars/production.yml's management_cidr — used for the VM firewall's SSH allow rule at the Proxmox level too, if managed there"
|
||||
type = string
|
||||
}
|
||||
|
||||
# ── rn-platform-01 sizing — baseline/recommended/max-growth (Phase 4 §3) ──────────────────
|
||||
variable "platform_vm" {
|
||||
description = "rn-platform-01 sizing. 'baseline' is the default; bump via terraform.tfvars once real hardware/load data exists."
|
||||
type = object({
|
||||
cores = number
|
||||
memory = number # MB
|
||||
disk_gb = number
|
||||
})
|
||||
default = {
|
||||
cores = 4
|
||||
memory = 16384
|
||||
disk_gb = 100
|
||||
}
|
||||
}
|
||||
|
||||
# ── rn-lab-host-01 sizing — majority of host resources, per the Phase 4 spec ───────────────
|
||||
variable "lab_host_vm" {
|
||||
description = "rn-lab-host-01 sizing. Deliberately NOT a final number — see docs/CAPACITY_PLANNING.md; this default is a conservative starting point for a 16c/32t/128GB class host, not a target."
|
||||
type = object({
|
||||
cores = number
|
||||
memory = number # MB
|
||||
disk_gb = number
|
||||
})
|
||||
default = {
|
||||
cores = 24
|
||||
memory = 98304
|
||||
disk_gb = 500
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5.0"
|
||||
required_providers {
|
||||
proxmox = {
|
||||
source = "bpg/proxmox"
|
||||
version = "~> 0.60"
|
||||
}
|
||||
}
|
||||
|
||||
# Local state by default — no paid backend required. Uncomment for remote state once/if
|
||||
# that's wanted; not needed to use this module.
|
||||
# backend "s3" {
|
||||
# bucket = "rootnode-terraform-state"
|
||||
# key = "production/terraform.tfstate"
|
||||
# region = "auto"
|
||||
# }
|
||||
}
|
||||
|
||||
provider "proxmox" {
|
||||
endpoint = var.proxmox_api_url
|
||||
api_token = var.proxmox_api_token
|
||||
insecure = var.proxmox_tls_insecure
|
||||
}
|
||||
Reference in New Issue
Block a user