Problem#
I built an infrastructure lab on a Proxmox server, with network segmentation similar to production. A MikroTik CHR router separates four VLANs, and four Linux VMs run the application, the database, monitoring and CI. The application is an API for accounts and idempotent transfers on PostgreSQL, intentionally simple, so the focus stays on the engineering around it.
Terraform creates the VMs and data disks, Ansible configures everything, and administrative access only goes through a WireGuard VPN.
The decision that matters most: the system disk is disposable and the data disk survives VM recreation. I tested it: I wrote a file with a checksum, destroyed the VM, recreated it, and the file was there with the same checksum.
Technical decisions and the reasoning#
- Data disk in a separate stack (
data/). Recreating the VM must not erase the database. The disk belongs to a stack withprevent_destroy, and the database VM only attaches that disk. It works like an EBS volume that outlives the instance. - Two stacks with separate state. Data changes rarely, VMs change often. The disk must exist before the VM, so the apply order matters.
- VMs in a
for_eachdriven by a variable map. Adding a VM is one line in thetfvars. - CHR treated as a “pet”. It is the network entry point: if it goes down, everything goes down. That is why it has
prevent_destroyand sits outside thefor_each. - Least-privilege firewall per flow. Only monitoring reaches the database for backups, and only CI reaches the app’s SSH. I validated it with isolation tests: blocked flows time out, allowed flows respond.
- Remote state on GitLab, with locking. Each stack has its own state.
- Secrets out of the code. Proxmox credentials live in environment variables, and passwords live in the Ansible vault.
Real challenges#
- Parallel clones on NFS were hanging. I limited the apply to
-parallelism=1and documented why. - The GitLab runner ignored
ansible.cfg. The/buildsdirectory was writable by everyone, and Ansible discarded the configuration. I fixed it by settingANSIBLE_CONFIGin the job. - Slow first boot. cloud-init runs
apt upgrade, so Ansible waits for the VM to finish.
What I would do differently#
- Automate the CHR configuration, with tests for the firewall rules. Today it was configured by hand.
- Assess the risk of
path_in_datastore, which is experimental in the bpg/proxmox provider, before relying on it in production.
Evidence#
Lab architecture#
Technologies#
Terraform, Ansible, Proxmox, MikroTik, Docker, GitLab CI, PostgreSQL, Prometheus, Grafana, Alertmanager, Trivy, FastAPI.