Scratch container lifecycle: create, configure, destroy from code
The full test loop for Proxmox infrastructure code — mint a scoped identity, tofu apply a throwaway container, converge it with Ansible, destroy every trace.
DistrosDebian 13, Proxmox VE 9
Shell
bashUpdated
Script
# Scratch-container lifecycle — proves your cluster code can BUILD.
# Full walkthrough: /blog/opentofu-scratch-container-lifecycle/
# Run each stage deliberately; this is a checklist, not a fire-and-forget.
### 1 · mint a throwaway identity (on a PVE node, as root)
pveum user add scratch@pve
pveum acl modify /vms/9001 --users scratch@pve --roles PVEAdmin
pveum acl modify /storage/local-lvm --users scratch@pve --roles PVEAdmin
pveum acl modify /nodes/pvelab01 --users scratch@pve --roles PVEAdmin
pveum acl modify / --users scratch@pve --roles PVEAuditor
pveum user token add scratch@pve tf --privsep 0
# copy the secret from the output — it is shown exactly once
### 2 · create the container (on your workstation, in terraform/scratch/)
export PROXMOX_VE_API_TOKEN='scratch@pve!tf=PASTE_SECRET_HERE'
tofu init && tofu apply
# expect: Apply complete! Resources: 1 added.
### 3 · wait for DHCP properly, then read the address (on the node)
pct exec 9001 -- ifup eth0
pct exec 9001 -- ip -4 addr show eth0
### 4 · configure it (workstation; note the trailing comma)
ansible-playbook scratch.yml -i "10.0.0.201,"
# expect: ok>0 changed>0 failed=0 — then SSH in and look yourself
### 5 · destroy it, and verify with a different tool
tofu destroy
pct list # on the node: 9001 gone
### 6 · leave nothing behind
pveum user delete scratch@pve # drops its ACLs and token too
pveum acl list # confirm no scratch entries remain
ssh-keygen -R 10.0.0.201 # stale host key off your workstation
unset PROXMOX_VE_API_TOKEN
### 7 · the point: production never noticed
# in your PRODUCTION terraform directory:
tofu plan # expect: "No changes."
What this does
Runs one throwaway LXC container through its entire life — created by OpenTofu, configured by Ansible, verified, destroyed, and swept up — to prove that your infrastructure code can actually build things, not just describe them. The final stage re-checks that production was untouched throughout. The reasoning behind every stage lives in A Proxmox Container Born, Configured, and Destroyed From Code.
Prerequisites
- A
terraform/scratch/configuration with its own state, defining one container with a VMID your production range never uses (the series uses9001) - A small
scratch.ymlAnsible playbook whose tasks each prove something: package-cache refresh (egress works), one package install (the apt path works), one file write (convergence works) - SSH key access: your public key injected into the container via the scratch config’s
user_accountblock
Notes
- Make these values your own:
9001is the scratch VMID;pvelab01,local-lvm, and10.0.0.201are my documentation node, storage, and DHCP address — substitute what your cluster and lease actually give you.scratch@pveis a temporary account name;PASTE_SECRET_HEREis the token secret shown once at minting — it lives in your shell for the duration of the loop and nowhere else, never in a file or repo. If a value looks specific to one machine, it’s a placeholder to change — not a literal to copy. - Why a separate user instead of a stronger token: Proxmox privilege-separated tokens get the intersection of token and user permissions — a write token minted under a read-only automation user can never write. A disposable user with path-scoped
PVEAdminkeeps your standing automation credential a pure auditor while giving the test exactly one container’s worth of blast radius. - Separate state is the safety wall: the scratch directory’s state contains one object, so
tofu destroy— which removes the objects managed by that particular configuration — cannot reach production even in principle. Never share a state file between throwaway and production resources. - DHCP, not static: if your gateway (like mine) only routes clients it leased, a static-IP scratch guest will fail its first
apt updatewith no obvious cause. The blockingifupis also the clean fix for “container exists before its lease does” — prefer commands that return when ready over sleeps. - This exact loop ran end-to-end on the real cluster on 2026-07-24: one added,
ok=3 changed=3 failed=0, one destroyed, ACLs clean, production plan stillNo changes.