Small IaC project focused on demonstrating the full automation flow: provisioning an Ubuntu 24.04 VM on Proxmox with Terraform, reading Proxmox credentials from Vault, and configuring the VM with Ansible to run Nginx in Docker.
- Proxmox
- Terraform
- HashiCorp Vault
- Ansible
- Docker
- Docker Compose
- Nginx
This project uses the bpg/proxmox Terraform provider:
https://registry.terraform.io/providers/bpg/proxmox/latest/docs
Provider used in this project:
proxmox = {
source = "bpg/proxmox"
version = "0.106.0"
}Terraform reads all .tf files in the same directory.
The main.tf file is kept as a short entrypoint note only.
The actual configuration is split into dedicated files:
providers.tf -> providers, Vault secret read, Proxmox provider config
variables.tf -> input variables
vm.tf -> Proxmox VM resource
outputs.tf -> Terraform outputs
This keeps the project structure simple and easy to read.
| Variable | Default | Description |
|---|---|---|
vault_mount |
secret |
Vault KV secrets engine name. |
proxmox_secret |
proxmox |
Vault secret name that contains the Proxmox credentials. |
proxmox_node |
pve |
Proxmox node where the VM will be created. |
ubuntu_template_id |
9000 |
Proxmox VM template ID used for cloning Ubuntu 24.04. |
vm_name |
tf-vm-docker-01 |
Name of the VM that Terraform creates. |
vm_id |
200 |
Proxmox VM ID. Must be unique in Proxmox. |
vm_ipv4_address |
10.0.20.201/24 |
Static IPv4 address assigned to the VM through cloud-init. |
vm_gateway |
10.0.20.1 |
Default gateway for the VM. |
vm_bridge |
vmbr0 |
Proxmox network bridge used by the VM. |
vm_username |
ubuntu |
Linux user created by cloud-init. |
ssh_public_key |
no default | Public SSH key injected into the VM for Ansible access. Marked as sensitive. |
The ssh_public_key value should usually be provided through terraform.tfvars or another local method.
Example:
ssh_public_key = "ssh-ed25519 AAAA..."Do not commit terraform.tfvars to Git.
Before running Terraform, Proxmox should have:
1. Ubuntu 24.04 cloud-init template
2. Proxmox API token
3. Storage available for the VM
4. Network bridge available for the VM
5. SSH public key for cloud-init access
Template used in this project:
OS: Ubuntu 24.04
Template ID: 9000
Default VM values:
VM name: tf-vm-docker-01
VM ID: 200
VM IP: 10.0.20.201/24
Gateway: 10.0.20.1
Bridge: vmbr0
User: ubuntu
Terraform connects to Proxmox with an API token.
The recommended setup is:
1. Create a dedicated Proxmox user for Terraform
2. Assign permissions to that user
3. Create an API token for that user
4. Store the token values in Vault
For a small homelab project, the simplest option is to create a dedicated user and assign the built-in PVEAdmin role.
Example:
pveum user add terraform@pve
pveum aclmod / -user terraform@pve -role PVEAdmin
pveum user token add terraform@pve terraform-token --privsep 0The full token ID will look like:
terraform@pve!terraform-token
When --privsep 0 is used, the token inherits the user's permissions.
You can also create the token from the Proxmox UI:
Datacenter -> Permissions -> API Tokens -> Add
Example token format:
User: terraform@pve
Token ID: terraform-token
Full token ID: terraform@pve!terraform-token
For a stricter setup, create a custom role instead of using PVEAdmin.
Example role:
pveum role add TerraformProv -privs "VM.Allocate VM.Audit VM.Clone VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Monitor VM.PowerMgmt Datastore.Audit Datastore.AllocateSpace Sys.Audit SDN.Use"
pveum user add terraform@pve
pveum aclmod / -user terraform@pve -role TerraformProv
pveum user token add terraform@pve terraform-token --privsep 0This custom role is a starting point.
Depending on your Proxmox storage, network, template, and provider configuration, you may need to adjust permissions.
Terraform needs permissions to:
clone VM templates
create VMs
configure VM hardware
configure disks
configure cloud-init
start and stop VMs
use the target storage
use the target network bridge
Proxmox credentials are stored in local HashiCorp Vault.
Vault runs locally on my machine.
Vault access is provided with a local zsh environment variable:
export VAULT_TOKEN="..."Vault KV engine:
secret
Vault secret path:
secret/proxmox
KV v2 API path:
secret/data/proxmox
Required Vault keys:
proxmox_api_url
proxmox_token_id
proxmox_token_secret
Example values:
proxmox_api_url = https://10.0.0.10:8006/api2/json
proxmox_token_id = terraform@pve!terraform-token
proxmox_token_secret = <PROXMOX_TOKEN_SECRET>
Example command:
vault kv put secret/proxmox \
proxmox_api_url="https://10.0.0.10:8006/api2/json" \
proxmox_token_id="terraform@pve!terraform-token" \
proxmox_token_secret="<PROXMOX_TOKEN_SECRET>"Terraform reads the Proxmox values from Vault:
provider "vault" {}
ephemeral "vault_kv_secret_v2" "proxmox" {
mount = var.vault_mount
name = var.proxmox_secret
}The Proxmox provider uses the Vault values:
provider "proxmox" {
endpoint = ephemeral.vault_kv_secret_v2.proxmox.data["proxmox_api_url"]
api_token = "${ephemeral.vault_kv_secret_v2.proxmox.data["proxmox_token_id"]}=${ephemeral.vault_kv_secret_v2.proxmox.data["proxmox_token_secret"]}"
insecure = true
}Run from the Terraform directory:
cd terraform
terraform init
terraform plan
terraform applyTerraform creates the VM in Proxmox.
Terraform outputs:
vm_name
vm_id
vm_ipv4_address
Ansible connects to the VM with a local SSH private key.
Example ansible/inventory.ini:
[web]
tf-vm-docker-01 ansible_host=10.0.20.201
[web:vars]
ansible_user=ubuntu
ansible_python_interpreter=/usr/bin/python3.12
ansible_ssh_private_key_file=~/.ssh/id_ed25519
nginx_port=80The private SSH key stays local and is not committed to Git.
Run from the Ansible directory:
cd ansibleBootstrap the VM:
ansible-playbook playbooks/bootstrap.yamlDeploy Docker and Nginx:
ansible-playbook playbooks/site.yamlCleanup Nginx:
ansible-playbook playbooks/cleanup.yamlbootstrap.yaml -> runs system role
site.yaml -> runs docker role and nginx role
cleanup.yaml -> removes the Nginx deployment
system -> apt update and apt upgrade
docker -> install Docker, Docker Compose, and check Docker status
nginx -> copy docker-compose template, run Nginx, and check HTTP 200
1. Create Ubuntu 24.04 cloud-init template in Proxmox
2. Create a Proxmox user, role and API token for Terraform
3. Store proxmox_api_url, proxmox_token_id, and proxmox_token_secret in Vault
4. Export VAULT_TOKEN locally in zsh
5. Run Terraform to create the VM
6. Run Ansible bootstrap
7. Run Ansible site
8. Open Nginx on port 80
9. Run Ansible cleanup when needed
10. Run terraform destroy when the VM is no longer needed
Do not commit:
terraform.tfstate
terraform.tfstate.backup
terraform.tfvars
Vault token
Proxmox token secret
SSH private key
Recommended .gitignore entries:
# Terraform
terraform/**/.terraform/*
terraform/**/*.tfstate
terraform/**/*.tfstate.*
terraform/**/*.tfvars
terraform/**/*.tfvars.json
terraform/**/crash.log
terraform/**/crash.*.log
terraform/**/*.tfplan
terraform/**/*.plan
terraform/**/*.tfoverride
terraform/**/*.tfoverride.json
terraform/**/*_override.tf
terraform/**/*_override.tf.json
# Ansible
ansible/**/*.retry
ansible/**/.ansible/
ansible/**/ansible.log
ansible/**/.vault_pass*
ansible/**/vault-password*
# Secrets / env
.env
.env.*
!.env.example
*.pem
*.key
# macOS / IDE
.DS_Store
.vscode/
.idea/This is a personal portfolio project.
PRs will not be reviewed or merged.